60 lines
1.6 KiB
TypeScript
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>
|
|
);
|
|
}
|