// Reveal-on-scroll primitive. Adds a fade + rise once the element enters viewport. function Reveal({ children, delay = 0, y = 24, className = '', style = {}, as: Tag = 'div' }) { const ref = React.useRef(null); const [shown, setShown] = React.useState(false); React.useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) { setShown(true); io.unobserve(el); } }); }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' }); io.observe(el); return () => io.disconnect(); }, []); return ( {children} ); } // Number that counts up when it enters viewport. Optional decimals and suffix/prefix. function CountUp({ to, from = 0, decimals = 0, duration = 1400, prefix = '', suffix = '', className = '', style = {} }) { const ref = React.useRef(null); const [val, setVal] = React.useState(from); const started = React.useRef(false); React.useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting && !started.current) { started.current = true; const start = performance.now(); const tick = (t) => { const p = Math.min(1, (t - start) / duration); const eased = 1 - Math.pow(1 - p, 3); setVal(from + (to - from) * eased); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); } }); }, { threshold: 0.3 }); io.observe(el); return () => io.disconnect(); }, [to, from, duration]); return ( {prefix}{val.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}{suffix} ); } // Animated sparkline that redraws itself function Sparkline({ width = 120, height = 30, color = '#9387FF', data }) { const ref = React.useRef(null); const pts = React.useMemo(() => data || Array.from({ length: 24 }, (_, i) => 20 + Math.sin(i * 0.7) * 8 + Math.random() * 10), [data]); const max = Math.max(...pts), min = Math.min(...pts); const path = pts.map((v, i) => { const x = (i / (pts.length - 1)) * width; const y = height - ((v - min) / (max - min || 1)) * (height - 4) - 2; return (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1); }).join(' '); return ( ); } Object.assign(window, { Reveal, CountUp, Sparkline });