// Shared hooks + helpers used by every section. const useReveal = (threshold = 0.15) => { const ref = React.useRef(null); const [shown, setShown] = React.useState(false); React.useEffect(() => { if (!ref.current) return; const io = new IntersectionObserver( ([e]) => { if (e.isIntersecting) { setShown(true); io.disconnect(); } }, { threshold } ); io.observe(ref.current); return () => io.disconnect(); }, [threshold]); return [ref, shown]; }; // Animated number counter (eases to target once visible) const Counter = ({ to, suffix = '', prefix = '', decimals = 0, duration = 1400, trigger = true }) => { const [val, setVal] = React.useState(0); React.useEffect(() => { if (!trigger) return; let raf, start; const tick = (t) => { if (!start) start = t; const p = Math.min(1, (t - start) / duration); const eased = 1 - Math.pow(1 - p, 3); setVal(to * eased); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [to, duration, trigger]); return <>{prefix}{val.toFixed(decimals)}{suffix}; }; // useTick — runs a counter at given interval, returns int const useTick = (interval = 100) => { const [t, setT] = React.useState(0); React.useEffect(() => { const id = setInterval(() => setT(x => x + 1), interval); return () => clearInterval(id); }, [interval]); return t; }; // Useful: clock that ticks every second const useClock = () => { const [t, setT] = React.useState(() => new Date()); React.useEffect(() => { const id = setInterval(() => setT(new Date()), 1000); return () => clearInterval(id); }, []); return t; }; // Mouse parallax — returns x/y normalized to -1..1 of the ref const useMouseParallax = () => { const ref = React.useRef(null); const [m, setM] = React.useState({ x: 0, y: 0 }); React.useEffect(() => { const el = ref.current; if (!el) return; const onMove = (e) => { const r = el.getBoundingClientRect(); const x = ((e.clientX - r.left) / r.width) * 2 - 1; const y = ((e.clientY - r.top) / r.height) * 2 - 1; setM({ x, y }); }; const onLeave = () => setM({ x: 0, y: 0 }); el.addEventListener('mousemove', onMove); el.addEventListener('mouseleave', onLeave); return () => { el.removeEventListener('mousemove', onMove); el.removeEventListener('mouseleave', onLeave); }; }, []); return [ref, m]; }; // Scroll-y normalized to viewport (for nav progress / parallax) const useScrollProgress = () => { const [p, setP] = React.useState(0); React.useEffect(() => { const onScroll = () => { const max = document.body.scrollHeight - window.innerHeight; setP(max > 0 ? window.scrollY / max : 0); }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); return p; }; // useResponsive — detect mobile viewport const useResponsive = () => { const [isMobile, setIsMobile] = React.useState(typeof window !== 'undefined' && window.innerWidth < 768); React.useEffect(() => { const handleResize = () => setIsMobile(window.innerWidth < 768); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return isMobile; }; // Reveal wrapper — fades + translates up when in view const Reveal = ({ children, delay = 0, y = 16, as: Tag = 'div', style }) => { const [ref, shown] = useReveal(0.12); return ( {children} ); }; Object.assign(window, { useReveal, Counter, useTick, useClock, useMouseParallax, useScrollProgress, Reveal, useResponsive });