// scroll-reveal.jsx // Utilities for scroll-driven animations on the landing page. // // useInView(ref, { threshold, once }) -> boolean // True when the element's intersection ratio crosses threshold. // // useScrollProgress(ref) -> number 0..1 // Progress of the element through the viewport (0 when entering, 1 when leaving). // // — simple fade-up on first intersection. const useInView = (ref, { threshold = 0.2, once = true } = {}) => { const [inView, setInView] = React.useState(false); React.useEffect(() => { if (!ref.current) return; const el = ref.current; const obs = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { setInView(true); if (once) obs.disconnect(); } else if (!once) { setInView(false); } }, { threshold, rootMargin: '0px 0px -10% 0px' }); obs.observe(el); return () => obs.disconnect(); }, [ref, threshold, once]); return inView; }; // Element progress 0..1: 0 when element top hits viewport bottom; 1 when element bottom hits viewport top const useScrollProgress = (ref) => { const [p, setP] = React.useState(0); React.useEffect(() => { if (!ref.current) return; const el = ref.current; let raf = null; const compute = () => { const r = el.getBoundingClientRect(); const vh = window.innerHeight; const total = r.height + vh; const passed = vh - r.top; setP(Math.max(0, Math.min(1, passed / total))); raf = null; }; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(compute); }; compute(); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); if (raf) cancelAnimationFrame(raf); }; }, [ref]); return p; }; // Simple "enter" wrapper — fade-up once in view. const Reveal = ({ as: Tag = 'div', delay = 0, y = 24, duration = 800, threshold = 0.2, className = '', style = {}, children, ...rest }) => { const ref = React.useRef(null); const inView = useInView(ref, { threshold, once: true }); return ( {children} ); }; // Run a callback repeatedly with requestAnimationFrame while `active`. const useRaf = (callback, active = true) => { const cbRef = React.useRef(callback); cbRef.current = callback; React.useEffect(() => { if (!active) return; let raf; let start = performance.now(); const loop = (t) => { cbRef.current((t - start) / 1000); raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); return () => cancelAnimationFrame(raf); }, [active]); }; Object.assign(window, { useInView, useScrollProgress, Reveal, useRaf });