// Arise Helix — shared primitives. const { useEffect, useRef, useState, useMemo, useCallback } = React; /* ──────────────── Scroll + viewport hooks ──────────────── */ function useScrollY() { const [y, setY] = useState(0); useEffect(() => { const onScroll = () => setY(window.scrollY); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); return y; } function useInViewOnce(rootMargin = "0px") { const ref = useRef(null); const [inView, setInView] = useState(false); useEffect(() => { if (!ref.current) return; const obs = new IntersectionObserver( (entries) => { entries.forEach((e) => { if (e.isIntersecting) { setInView(true); obs.disconnect(); } }); }, { rootMargin, threshold: 0.05 } ); obs.observe(ref.current); return () => obs.disconnect(); }, [rootMargin]); return [ref, inView]; } function useScrollProgressOn(ref, startVP = 0.85, endVP = 0.2) { const [progress, setProgress] = useState(0); useEffect(() => { if (!ref.current) return; const onScroll = () => { const el = ref.current; if (!el) return; const rect = el.getBoundingClientRect(); const vh = window.innerHeight; const startScrollY = window.scrollY + (rect.top - vh * startVP); const endScrollY = window.scrollY + (rect.bottom - vh * endVP); const total = endScrollY - startScrollY; if (total <= 0) return setProgress(0); const p = (window.scrollY - startScrollY) / total; setProgress(Math.max(0, Math.min(1, p))); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, [ref, startVP, endVP]); return progress; } // Section index detector — which of the registered sections is currently dominant in the viewport function useActiveSection(sectionIds) { const [active, setActive] = useState(0); useEffect(() => { const onScroll = () => { const vh = window.innerHeight; const center = window.scrollY + vh / 2; let bestIdx = 0; let bestDist = Infinity; sectionIds.forEach((id, i) => { const el = document.getElementById(id); if (!el) return; const top = el.offsetTop; const bottom = top + el.offsetHeight; const sectionCenter = (top + bottom) / 2; const dist = Math.abs(sectionCenter - center); if (dist < bestDist) { bestDist = dist; bestIdx = i; } }); setActive(bestIdx); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, [JSON.stringify(sectionIds)]); return active; } // Page scroll progress 0..1 across whole document function usePageProgress() { const [p, setP] = useState(0); useEffect(() => { const onScroll = () => { const total = document.body.scrollHeight - window.innerHeight; if (total <= 0) return setP(0); setP(Math.max(0, Math.min(1, window.scrollY / total))); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, []); return p; } // Cursor position (for magnetic / radial glow) function useCursor() { const [pos, setPos] = useState({ x: -1, y: -1 }); useEffect(() => { const onMove = (e) => setPos({ x: e.clientX, y: e.clientY }); window.addEventListener("pointermove", onMove); return () => window.removeEventListener("pointermove", onMove); }, []); return pos; } /* ──────────────── Text reveal primitives ──────────────── */ function WordsPullUp({ text, className = "", style = {}, stagger = 0.06, delayBase = 0 }) { const [ref, inView] = useInViewOnce("-40px"); const words = useMemo(() => text.split(" "), [text]); return (
{chars.map((c, i) => { const cp = i / total; const start = cp - 0.1; const end = cp + 0.05; let o = 0.15; if (progress >= end) o = 1; else if (progress > start) o = 0.15 + ((progress - start) / (end - start)) * 0.85; return ( {c} ); })}
); } function FadeUp({ children, delay = 0, y = 22, className = "", style = {}, as = "div" }) { const [ref, inView] = useInViewOnce("-40px"); const Tag = as; return (