// HpInteractions.jsx — foreground interactive motion. // 1) CursorBlob — soft coral spotlight that follows the mouse (with easing lag) // 2) Magnet — .hp-btn / .hp-magnet subtly pull toward cursor within 100px // 3) Tilt — .hp-tilt cards rotate 8° in 3D, coral highlight sweeps at cursor // // All effects disabled on touch devices and when prefers-reduced-motion is set. const _hpFineHover = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches; const _hpReducedMotion = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const _hpInteractive = _hpFineHover && !_hpReducedMotion; // ───────────────────────────────────────────────────────── // CursorBlob — soft coral spotlight + tiny inner dot // ───────────────────────────────────────────────────────── function CursorBlob() { const dotRef = React.useRef(null); const blobRef = React.useRef(null); const target = React.useRef({ x: -200, y: -200 }); const smooth = React.useRef({ x: -200, y: -200 }); const [visible, setVisible] = React.useState(false); React.useEffect(() => { if (!_hpInteractive) return; const onMove = (e) => { target.current.x = e.clientX; target.current.y = e.clientY; if (!visible) setVisible(true); }; const onLeave = () => setVisible(false); window.addEventListener("mousemove", onMove, { passive: true }); document.addEventListener("mouseleave", onLeave); let rafId; const tick = () => { // Ease the blob toward the pointer for the lag effect smooth.current.x += (target.current.x - smooth.current.x) * 0.16; smooth.current.y += (target.current.y - smooth.current.y) * 0.16; if (dotRef.current) { dotRef.current.style.transform = "translate3d(" + target.current.x + "px, " + target.current.y + "px, 0) translate(-50%, -50%)"; } if (blobRef.current) { blobRef.current.style.transform = "translate3d(" + smooth.current.x + "px, " + smooth.current.y + "px, 0) translate(-50%, -50%)"; } rafId = requestAnimationFrame(tick); }; rafId = requestAnimationFrame(tick); return () => { window.removeEventListener("mousemove", onMove); document.removeEventListener("mouseleave", onLeave); cancelAnimationFrame(rafId); }; }, [visible]); if (!_hpInteractive) return null; return ( <>
> ); } window.CursorBlob = CursorBlob; // ───────────────────────────────────────────────────────── // Magnet — global handler for .hp-btn and .hp-magnet // Sets --magnet-x / --magnet-y CSS vars, consumed by their transform. // ───────────────────────────────────────────────────────── (function setupMagnet() { if (typeof window === "undefined") return; if (!_hpInteractive) return; const RADIUS = 110; const MAX_PULL = 14; const onMove = (e) => { const els = document.querySelectorAll(".hp-btn, .hp-magnet"); els.forEach((el) => { const rect = el.getBoundingClientRect(); const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height / 2; const dx = e.clientX - cx; const dy = e.clientY - cy; const dist = Math.hypot(dx, dy); if (dist < RADIUS) { const strength = 1 - dist / RADIUS; el.style.setProperty("--magnet-x", (dx / RADIUS) * MAX_PULL * strength + "px"); el.style.setProperty("--magnet-y", (dy / RADIUS) * MAX_PULL * strength + "px"); } else if (el.style.getPropertyValue("--magnet-x") !== "0px") { el.style.setProperty("--magnet-x", "0px"); el.style.setProperty("--magnet-y", "0px"); } }); }; const install = () => window.addEventListener("mousemove", onMove, { passive: true }); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", install); } else { install(); } })(); // ───────────────────────────────────────────────────────── // Tilt — 3D rotation + cursor-tracked coral highlight on .hp-tilt // Uses per-element listeners; auto-binds new elements via MutationObserver. // ───────────────────────────────────────────────────────── (function setupTilt() { if (typeof window === "undefined") return; if (!_hpInteractive) return; const MAX_TILT = 7; // degrees const bind = (el) => { if (el.hasAttribute("data-tilt-bound")) return; el.setAttribute("data-tilt-bound", "true"); el.addEventListener("mousemove", (e) => { const rect = el.getBoundingClientRect(); const px = (e.clientX - rect.left) / rect.width; // 0..1 const py = (e.clientY - rect.top) / rect.height; // 0..1 const rotX = (0.5 - py) * MAX_TILT * 2; const rotY = (px - 0.5) * MAX_TILT * 2; el.style.setProperty("--tilt-x", rotX + "deg"); el.style.setProperty("--tilt-y", rotY + "deg"); el.style.setProperty("--glow-x", px * 100 + "%"); el.style.setProperty("--glow-y", py * 100 + "%"); }, { passive: true }); el.addEventListener("mouseleave", () => { el.style.setProperty("--tilt-x", "0deg"); el.style.setProperty("--tilt-y", "0deg"); }); }; const bindAll = () => { document.querySelectorAll(".hp-tilt").forEach(bind); }; const install = () => { bindAll(); // Bind cards that mount later (React re-renders) const mo = new MutationObserver(bindAll); mo.observe(document.body, { childList: true, subtree: true }); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", install); } else { install(); } })(); // ───────────────────────────────────────────────────────── // Hero scroll progress — sets --scroll-progress on the hero element // consumed by CSS for parallax + underline draw. // ───────────────────────────────────────────────────────── function useHeroScrollProgress(ref) { React.useEffect(() => { const el = ref.current; if (!el) return; if (_hpReducedMotion) { el.style.setProperty("--scroll-progress", "0"); el.style.setProperty("--underline-progress", "1"); return; } let rafPending = false; const update = () => { rafPending = false; const rect = el.getBoundingClientRect(); const viewH = window.innerHeight || 800; // Parallax progress: 0 while hero fully visible, 1 when hero fully scrolled past const scrolled = Math.max(0, -rect.top); const parallaxProgress = Math.min(1, scrolled / (rect.height || 1)); el.style.setProperty("--scroll-progress", parallaxProgress.toFixed(3)); // Underline progress: 0 when hero enters viewport (top at bottom), 1 by mid-scroll const enteredBy = viewH - rect.top; const underlineProgress = Math.max(0, Math.min(1, enteredBy / (viewH * 0.55))); el.style.setProperty("--underline-progress", underlineProgress.toFixed(3)); }; const onScroll = () => { if (rafPending) return; rafPending = true; requestAnimationFrame(update); }; update(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, [ref]); } window.useHeroScrollProgress = useHeroScrollProgress;