agency-web/app/components/Magnetic.tsx

60 lines
1.6 KiB
TypeScript

"use client";
/**
* Magnetic — wraps a single interactive child and pulls it toward the cursor
* within a radius, springing back on leave. Disabled for coarse pointers and
* reduced-motion. Purely visual; does not alter semantics of the child.
*/
import { useEffect, useRef, type ReactNode } from "react";
import { gsap } from "gsap";
export default function Magnetic({
children,
strength = 0.4,
className = "",
}: {
children: ReactNode;
strength?: number;
className?: string;
}) {
const ref = useRef<HTMLSpanElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (window.matchMedia("(pointer: coarse)").matches) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const target = el.firstElementChild as HTMLElement | null;
if (!target) return;
const xTo = gsap.quickTo(target, "x", { duration: 0.5, ease: "elastic.out(1, 0.4)" });
const yTo = gsap.quickTo(target, "y", { duration: 0.5, ease: "elastic.out(1, 0.4)" });
const move = (e: MouseEvent) => {
const r = el.getBoundingClientRect();
const mx = e.clientX - (r.left + r.width / 2);
const my = e.clientY - (r.top + r.height / 2);
xTo(mx * strength);
yTo(my * strength);
};
const leave = () => {
xTo(0);
yTo(0);
};
el.addEventListener("mousemove", move);
el.addEventListener("mouseleave", leave);
return () => {
el.removeEventListener("mousemove", move);
el.removeEventListener("mouseleave", leave);
};
}, [strength]);
return (
<span ref={ref} className={`magnetic ${className}`}>
{children}
</span>
);
}