import { useEffect, useRef, useCallback } from "react"; interface LazyEyesOptions { /** Reference point the eyes "live" at (viewport coords). Eyes look away from this toward the mouse. */ anchorRef: React.RefObject<{ x: number; y: number } | null>; /** Max pixel shift for the iris (default 1.5) */ maxShift?: number; /** Lerp ease factor 0–1 for slow drift (default 0.04) */ ease?: number; /** Saccade threshold — when target jumps more than this, snap fast (default 0.8) */ saccadeThreshold?: number; /** Fast ease for saccade snap (default 0.35) */ saccadeEase?: number; } /** * Returns a register function to attach iris elements for direct DOM updates. * No React state is set per frame — transforms are applied directly. * * Movement model: * - Small mouse moves → slow, lazy drift (ease) * - Large jumps → quick saccade snap (saccadeEase), then settle * - Tiny random micro-drift to avoid perfectly still eyes */ export function useLazyEyes({ anchorRef, maxShift = 1.5, ease = 0.04, saccadeThreshold = 0.8, saccadeEase = 0.35, }: LazyEyesOptions) { const targetRef = useRef({ x: 0, y: 0 }); const currentRef = useRef({ x: 0, y: 0 }); const velocityRef = useRef({ x: 0, y: 0 }); const irisesRef = useRef>(new Set()); const offsetRef = useRef({ x: 0, y: 0 }); const registerIris = useCallback((el: HTMLElement | null) => { if (el) { irisesRef.current.add(el); } }, []); const unregisterIris = useCallback((el: HTMLElement | null) => { if (el) { irisesRef.current.delete(el); } }, []); useEffect(() => { const onMouseMove = (e: MouseEvent) => { const anchor = anchorRef.current; if (!anchor) return; const dx = e.clientX - anchor.x; const dy = e.clientY - anchor.y; const dist = Math.sqrt(dx * dx + dy * dy) || 1; targetRef.current = { x: (dx / dist) * maxShift, y: (dy / dist) * maxShift, }; }; window.addEventListener("mousemove", onMouseMove); let raf = 0; const tick = () => { const ec = currentRef.current; const et = targetRef.current; const vel = velocityRef.current; const dx = et.x - ec.x; const dy = et.y - ec.y; const dist = Math.sqrt(dx * dx + dy * dy); const e_ = dist > saccadeThreshold ? saccadeEase : ease; vel.x = vel.x * 0.6 + dx * e_ * 0.4; vel.y = vel.y * 0.6 + dy * e_ * 0.4; ec.x += vel.x; ec.y += vel.y; if (dist < 0.1) { ec.x += (Math.random() - 0.5) * 0.02; ec.y += (Math.random() - 0.5) * 0.02; } offsetRef.current.x = ec.x; offsetRef.current.y = ec.y; // Direct DOM updates — no React re-render for (const iris of irisesRef.current) { iris.style.transform = `translate(${ec.x}px, ${ec.y}px)`; } raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => { window.removeEventListener("mousemove", onMouseMove); cancelAnimationFrame(raf); }; }, [anchorRef, maxShift, ease, saccadeThreshold, saccadeEase]); return { offsetRef, registerIris, unregisterIris }; }