const { useState, useEffect, useRef } = React; /* Scroll reveal: fade + rise + blur, once. */ function useReveal(delay = 0) { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setShown(true); return; } const el = ref.current; if (!el) return; const io = new IntersectionObserver((es) => { es.forEach((e) => { if (e.isIntersecting) { setTimeout(() => setShown(true), delay); io.disconnect(); } }); }, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" }); io.observe(el); return () => io.disconnect(); }, [delay]); return [ref, shown]; } function Reveal({ children, delay = 0, y = 22, as = "div", style, ...rest }) { const [ref, shown] = useReveal(delay); const Tag = as; return React.createElement(Tag, { ref, ...rest, style: { opacity: shown ? 1 : 0, transform: shown ? "none" : `translateY(${y}px)`, filter: shown ? "blur(0)" : "blur(6px)", transition: "opacity .7s cubic-bezier(.2,.7,.3,1), transform .7s cubic-bezier(.2,.7,.3,1), filter .7s cubic-bezier(.2,.7,.3,1)", ...style } }, children); } /* Count up to a target once visible. Preserves suffixes like K and +. */ function Counter({ value, duration = 1200 }) { const m = String(value).match(/^([\d.]+)(.*)$/); const target = m ? parseFloat(m[1]) : 0; const suffix = m ? m[2] : ""; const decimals = m && m[1].includes(".") ? 1 : 0; const [ref, shown] = useReveal(0); const [n, setN] = useState(0); useEffect(() => { if (!shown) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setN(target); return; } let raf, t0; const step = (t) => { if (!t0) t0 = t; const p = Math.min(1, (t - t0) / duration); setN(target * (1 - Math.pow(1 - p, 3))); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, [shown, target, duration]); return {n.toFixed(decimals)}{suffix}; } /* Parallax translate driven by scroll position. */ function useParallax(strength = 0.12) { const ref = useRef(null); useEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; let raf = 0; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = 0; const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); const mid = r.top + r.height / 2 - window.innerHeight / 2; el.style.transform = `translate3d(0, ${(-mid * strength).toFixed(1)}px, 0)`; }); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => { window.removeEventListener("scroll", onScroll); if (raf) cancelAnimationFrame(raf); }; }, [strength]); return ref; } /* Pointer-following glow, confined to the element. */ function GlowSurface({ children, radius = 420, style, ...rest }) { const ref = useRef(null); const onMove = (e) => { const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); el.style.setProperty("--gx", `${e.clientX - r.left}px`); el.style.setProperty("--gy", `${e.clientY - r.top}px`); el.style.setProperty("--go", "1"); }; const onLeave = () => { const el = ref.current; if (el) el.style.setProperty("--go", "0"); }; return (
{children}
); } /* Card tilt on pointer. */ function Tilt({ children, max = 6, style, ...rest }) { const ref = useRef(null); const reduced = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const onMove = (e) => { if (reduced) return; const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); const px = (e.clientX - r.left) / r.width - 0.5; const py = (e.clientY - r.top) / r.height - 0.5; el.style.transform = `perspective(900px) rotateX(${(-py * max).toFixed(2)}deg) rotateY(${(px * max).toFixed(2)}deg) translateY(-2px)`; }; const onLeave = () => { const el = ref.current; if (el) el.style.transform = "perspective(900px) rotateX(0) rotateY(0) translateY(0)"; }; return
{children}
; } /* Soft cursor companion — a lagging accent halo, desktop pointers only. */ function CursorGlow() { useEffect(() => { if (window.matchMedia("(pointer: coarse)").matches) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; const dot = document.createElement("div"); dot.className = "cursor-halo"; document.body.appendChild(dot); let x = window.innerWidth / 2, y = window.innerHeight / 2, cx = x, cy = y, raf; const move = (e) => { x = e.clientX; y = e.clientY; dot.style.opacity = "1"; }; const over = (e) => { dot.dataset.on = e.target.closest("a,button,input,textarea,[role=button]") ? "1" : "0"; }; const loop = () => { cx += (x - cx) * 0.14; cy += (y - cy) * 0.14; dot.style.transform = `translate3d(${cx}px, ${cy}px, 0) translate(-50%,-50%)`; raf = requestAnimationFrame(loop); }; loop(); window.addEventListener("mousemove", move, { passive: true }); window.addEventListener("mouseover", over, { passive: true }); return () => { cancelAnimationFrame(raf); window.removeEventListener("mousemove", move); window.removeEventListener("mouseover", over); dot.remove(); }; }, []); return null; } /* Rotating word in the hero. */ function Rotator({ words, interval = 2400 }) { const [i, setI] = useState(0); const [out, setOut] = useState(false); useEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; const t = setInterval(() => { setOut(true); setTimeout(() => { setI((v) => (v + 1) % words.length); setOut(false); }, 340); }, interval); return () => clearInterval(t); }, [words.length, interval]); /* Sizer holds the tallest word so the headline never changes height mid-rotation. */ return ( {words.map((w) => )} {words[i]} ); } Object.assign(window, { useReveal, Reveal, Counter, useParallax, GlowSurface, Tilt, CursorGlow, Rotator });