/* Shared hooks + utilities */ const { useState, useEffect, useRef, useLayoutEffect, useMemo, useCallback } = React; // Scroll Y + direction function useScroll() { const [y, setY] = useState(0); const [dir, setDir] = useState('down'); const lastY = useRef(0); useEffect(() => { const onScroll = () => { const cy = window.scrollY; setY(cy); setDir(cy > lastY.current ? 'down' : 'up'); lastY.current = cy; }; window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); return { y, dir }; } // Normalized mouse position (-0.5..0.5) function useMouse() { const [m, setM] = useState({ x: 0, y: 0, rx: 0, ry: 0 }); useEffect(() => { const onMove = (e) => { const rx = (e.clientX / window.innerWidth) - 0.5; const ry = (e.clientY / window.innerHeight) - 0.5; setM({ x: e.clientX, y: e.clientY, rx, ry }); }; window.addEventListener('mousemove', onMove, { passive: true }); return () => window.removeEventListener('mousemove', onMove); }, []); return m; } // IntersectionObserver reveal function useReveal(options = {}) { const ref = useRef(null); const [visible, setVisible] = useState(false); useEffect(() => { if (!ref.current) return; const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) setVisible(true); }, { threshold: 0.15, ...options }); io.observe(ref.current); return () => io.disconnect(); }, []); return [ref, visible]; } // Counter that animates when visible function useCounter(target, { duration = 1800, decimals = 0, start = false } = {}) { const [val, setVal] = useState(0); useEffect(() => { if (!start) return; let raf; const t0 = performance.now(); const tick = (t) => { const p = Math.min(1, (t - t0) / duration); const eased = 1 - Math.pow(1 - p, 4); setVal(target * eased); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [start, target, duration]); return decimals ? val.toFixed(decimals) : Math.round(val).toLocaleString('es-CL'); } // Element-relative scroll progress (0..1 as element moves through viewport) function useElementScroll() { const ref = useRef(null); const [p, setP] = useState(0); useEffect(() => { const onScroll = () => { if (!ref.current) return; const r = ref.current.getBoundingClientRect(); const vh = window.innerHeight; // starts when top enters, ends when bottom leaves const total = r.height + vh; const progressed = vh - r.top; setP(Math.max(0, Math.min(1, progressed / total))); }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); }; }, []); return [ref, p]; } // Persist in localStorage function usePersisted(key, initial) { const [v, setV] = useState(() => { try { const s = localStorage.getItem(key); return s !== null ? JSON.parse(s) : initial; } catch { return initial; } }); useEffect(() => { try { localStorage.setItem(key, JSON.stringify(v)); } catch {} }, [key, v]); return [v, setV]; } const lerp = (a, b, t) => a + (b - a) * t; const clamp = (v, a, b) => Math.max(a, Math.min(b, v)); Object.assign(window, { useScroll, useMouse, useReveal, useCounter, useElementScroll, usePersisted, lerp, clamp });