import { useEffect, useRef, type CSSProperties, type ReactNode, } from 'react'; import { prefersReducedMotion, SPRINGS } from './utils.js'; export interface MagneticProps { /** Child element — typically a button or link. */ children: ReactNode; /** Maximum translation in px. Default 12. */ strength?: number; /** Activation radius around the element. Default 120 px. */ radius?: number; /** Bypass motion entirely (renders without transform). */ disableMotion?: boolean; className?: string; style?: CSSProperties; } /** * `` — Arc-browser-style pointer attraction wrapper. As the * cursor enters a `radius` around the element, the child translates * toward it (clamped to `strength`). Resets smoothly on pointer leave. * * Implementation note: we attach pointer listeners to **window**, not * the wrapped element, so the magnet has a "field" — the cursor pulls * the child even before hovering it. */ export function Magnetic({ children, strength = 12, radius = 120, disableMotion, className, style, }: MagneticProps) { const ref = useRef(null); const reduced = disableMotion ?? prefersReducedMotion(); useEffect(() => { if (reduced) return; const el = ref.current; if (!el) return; const onMove = (e: PointerEvent) => { 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) { el.style.transform = 'translate3d(0,0,0)'; return; } const f = (1 - dist / radius) * strength; const tx = (dx / Math.max(1, dist)) * f; const ty = (dy / Math.max(1, dist)) * f; el.style.transform = `translate3d(${tx}px, ${ty}px, 0)`; }; const onLeave = () => { el.style.transform = 'translate3d(0,0,0)'; }; window.addEventListener('pointermove', onMove); window.addEventListener('pointerleave', onLeave); return () => { window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerleave', onLeave); }; }, [reduced, radius, strength]); return ( {children} ); }