// ─── UI UTILITIES ────────────────────────────────────────────────────── const { useEffect, useRef, useState, useMemo, useCallback } = React; function CursorDot() { const ref = useRef(null); const [visible, setVisible] = useState(false); const [expand, setExpand] = useState(false); useEffect(() => { let raf = 0; let tx = 0, ty = 0, cx = 0, cy = 0; const onMove = (e) => { tx = e.clientX; ty = e.clientY; setVisible(true); }; const onLeave = () => setVisible(false); const sel = "a, button, .btn, .case-card, .specialty-card, .plan-card, .t-card, [data-magnetic]"; const onOver = (e) => { if (e.target.closest?.(sel)) setExpand(true); }; const onOut = (e) => { if (e.target.closest?.(sel)) setExpand(false); }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseleave", onLeave); document.addEventListener("mouseover", onOver); document.addEventListener("mouseout", onOut); const tick = () => { cx += (tx - cx) * 0.22; cy += (ty - cy) * 0.22; if (ref.current) ref.current.style.transform = `translate(${cx}px, ${cy}px) translate(-50%, -50%)`; raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseleave", onLeave); document.removeEventListener("mouseover", onOver); document.removeEventListener("mouseout", onOut); }; }, []); return
; } function useReveal() { useEffect(() => { const els = document.querySelectorAll(".reveal"); const fallback = setTimeout(() => els.forEach(el => el.classList.add("in")), 600); let io; try { io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } }); }, { rootMargin: "0px 0px -10% 0px", threshold: 0.01 }); els.forEach(el => io.observe(el)); } catch { els.forEach(el => el.classList.add("in")); } return () => { clearTimeout(fallback); if (io) io.disconnect(); }; }, []); } function AnimatedNumber({ to, prefix = "", suffix = "", duration = 1400, decimals = 0 }) { const ref = useRef(null); const [val, setVal] = useState(0); const [started, setStarted] = useState(false); useEffect(() => { if (!ref.current) return; const io = new IntersectionObserver(([e]) => { if (e.isIntersecting && !started) setStarted(true); }, { threshold: 0.4 }); io.observe(ref.current); return () => io.disconnect(); }, [started]); useEffect(() => { if (!started) return; const t0 = performance.now(); let raf = 0; const tick = (t) => { const p = Math.min(1, (t - t0) / duration); const eased = 1 - Math.pow(1 - p, 4); setVal(to * eased); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [started, to, duration]); const formatted = useMemo(() => { const n = decimals > 0 ? val.toFixed(decimals) : Math.round(val).toString(); return n.replace(/\B(?=(\d{3})+(?!\d))/g, "."); }, [val, decimals]); return {prefix}{formatted}{suffix}; } function Marquee({ items, renderItem, gap = 56, duration = 60, className = "" }) { const doubled = [...items, ...items]; return (