// shared hooks + utilities const { useEffect, useRef, useState, useMemo, useCallback } = React; function useInView(options = {}) { const ref = useRef(null); const [inView, setInView] = useState(false); useEffect(() => { if (!ref.current) return; const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setInView(true); io.disconnect(); } }, { threshold: 0.15, ...options }); io.observe(ref.current); return () => io.disconnect(); }, []); return [ref, inView]; } function useReveal() { useEffect(() => { const els = Array.from(document.querySelectorAll('.reveal')); const reveal = (el) => el.classList.add('in'); // If an element is already in viewport (or scrolled past it) at mount, // reveal immediately. Browsers can restore scroll or jump to a hash before // the IO's initial callback fires, which previously left those elements // stuck at opacity:0. const revealIfVisibleOrPast = (el) => { const r = el.getBoundingClientRect(); const vh = window.innerHeight || document.documentElement.clientHeight; // Visible now OR already scrolled past the top of the viewport if (r.top < vh && r.bottom > -200) reveal(el); }; const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { reveal(e.target); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -60px 0px' }); const observe = () => { els.forEach(el => { if (el.classList.contains('in')) return; revealIfVisibleOrPast(el); if (!el.classList.contains('in')) io.observe(el); }); }; observe(); // Re-check after fonts/images settle in case initial layout shifted things. const t1 = setTimeout(observe, 250); const t2 = setTimeout(observe, 1200); const onLoad = () => observe(); window.addEventListener('load', onLoad); return () => { clearTimeout(t1); clearTimeout(t2); window.removeEventListener('load', onLoad); io.disconnect(); }; }, []); } function useCounter(target, inView, duration = 1800) { const [val, setVal] = useState(0); useEffect(() => { if (!inView) return; let start; let raf; const tick = (t) => { if (!start) start = t; const p = Math.min((t - start) / duration, 1); const eased = 1 - Math.pow(1 - p, 3); setVal(target * eased); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [inView, target, duration]); return val; } // SVG icons const Icon = { arrow: (p) => ( ), arrowUR: (p) => ( ), check: (p) => ( ), star: (p) => ( ), globe: (p) => ( ), layers: (p) => ( ), cloud: (p) => ( ), }; window.useInView = useInView; window.useReveal = useReveal; window.useCounter = useCounter; window.Icon = Icon;