// 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 (
{words.map((w, i) => ( {w} ))}
); } function MixedHeading({ parts, className = "", style = {}, stagger = 0.06 }) { // parts: [{ text, italic }] const [ref, inView] = useInViewOnce("-40px"); const items = useMemo(() => { const out = []; parts.forEach((p) => { p.text.split(" ").filter((w) => w.length).forEach((w) => out.push({ w, italic: !!p.italic })); }); return out; }, [parts]); return (

{items.map((it, i) => ( {it.w} ))}

); } function AnimatedLetters({ text, className = "", style = {} }) { const ref = useRef(null); const progress = useScrollProgressOn(ref, 0.8, 0.25); const chars = useMemo(() => Array.from(text), [text]); const total = chars.length; 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 ( {children} ); } function InViewScale({ children, delay = 0, className = "", style = {}, from = 0.96 }) { const [ref, inView] = useInViewOnce("-80px"); return (
{children}
); } function StatCounter({ to, suffix = "", duration = 1800, className = "", style = {}, formatter }) { const [ref, inView] = useInViewOnce("-60px"); const [val, setVal] = useState(0); useEffect(() => { if (!inView) return; const start = performance.now(); let raf; const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setVal(to * eased); if (t < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [inView, to, duration]); const display = formatter ? formatter(val) : Math.round(val).toLocaleString(); return {display}{suffix}; } /* ──────────────── Magnetic button ──────────────── */ function MagneticButton({ children, className = "", style = {}, onClick, intensity = 0.4 }) { const ref = useRef(null); const [t, setT] = useState({ x: 0, y: 0 }); const onMove = (e) => { const r = ref.current.getBoundingClientRect(); const cx = r.left + r.width / 2; const cy = r.top + r.height / 2; setT({ x: (e.clientX - cx) * intensity, y: (e.clientY - cy) * intensity }); }; const reset = () => setT({ x: 0, y: 0 }); return ( ); } /* ──────────────── Icons (lucide-style) ──────────────── */ const iconBase = { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round" }; const I = (size = 18, sx = {}, cls = "") => ({ ...iconBase, width: size, height: size, style: sx, className: cls }); const IconArrowRight = ({ size, style, className }) => (); const IconArrowUpRight = ({ size, style, className }) => (); const IconCheck = ({ size, style, className }) => (); const IconPlay = ({ size, style, className }) => (); const IconStethoscope = ({ size, style, className }) => (); const IconBookOpen = ({ size, style, className }) => (); const IconLayers = ({ size, style, className }) => (); const IconCalendar = ({ size, style, className }) => (); const IconSmartphone = ({ size, style, className }) => (); const IconGlobe = ({ size, style, className }) => (); const IconActivity = ({ size, style, className }) => (); const IconBeaker = ({ size, style, className }) => (); const IconWaveform = ({ size, style, className }) => (); const IconPhone = ({ size, style, className }) => (); const IconMail = ({ size, style, className }) => (); const IconMapPin = ({ size, style, className }) => (); const IconQuote = ({ size, style, className }) => (); const IconSparkle = ({ size, style, className }) => (); Object.assign(window, { useScrollY, useInViewOnce, useScrollProgressOn, useActiveSection, usePageProgress, useCursor, WordsPullUp, MixedHeading, AnimatedLetters, FadeUp, InViewScale, StatCounter, MagneticButton, IconArrowRight, IconArrowUpRight, IconCheck, IconPlay, IconStethoscope, IconBookOpen, IconLayers, IconCalendar, IconSmartphone, IconGlobe, IconActivity, IconBeaker, IconWaveform, IconPhone, IconMail, IconMapPin, IconQuote, IconSparkle, });