61 lines
1.9 KiB
TypeScript
61 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Magnetic wrapper — the child is pulled toward the cursor while hovered, then
|
|
* springs back with momentum on leave (loose spring => visible overshoot, the
|
|
* "alive" feel). The label inside also drifts at a deeper strength for a subtle
|
|
* parallax between the button and its text.
|
|
* Transform-only (GPU). No-op on coarse pointers and with prefers-reduced-motion
|
|
* so keyboard/touch users get a static, fully-clickable element.
|
|
*/
|
|
import { useRef } from "react";
|
|
import { motion, useMotionValue, useSpring, useTransform } from "motion/react";
|
|
import { SPRING } from "./motion";
|
|
|
|
export default function Magnetic({
|
|
children,
|
|
strength = 0.35,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode;
|
|
strength?: number;
|
|
className?: string;
|
|
}) {
|
|
const ref = useRef<HTMLSpanElement>(null);
|
|
const x = useMotionValue(0);
|
|
const y = useMotionValue(0);
|
|
const sx = useSpring(x, SPRING.magnetic);
|
|
const sy = useSpring(y, SPRING.magnetic);
|
|
// Inner content drifts a touch further => parallax depth between shell + label.
|
|
const innerX = useTransform(sx, (v) => v * 0.35);
|
|
const innerY = useTransform(sy, (v) => v * 0.35);
|
|
|
|
const onMove = (e: React.PointerEvent) => {
|
|
if (e.pointerType !== "mouse") return;
|
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
const r = el.getBoundingClientRect();
|
|
x.set((e.clientX - (r.left + r.width / 2)) * strength);
|
|
y.set((e.clientY - (r.top + r.height / 2)) * strength);
|
|
};
|
|
|
|
const reset = () => {
|
|
x.set(0);
|
|
y.set(0);
|
|
};
|
|
|
|
return (
|
|
<motion.span
|
|
ref={ref}
|
|
className={className}
|
|
onPointerMove={onMove}
|
|
onPointerLeave={reset}
|
|
style={{ x: sx, y: sy, display: "inline-block" }}
|
|
>
|
|
<motion.span style={{ x: innerX, y: innerY, display: "inline-block" }}>
|
|
{children}
|
|
</motion.span>
|
|
</motion.span>
|
|
);
|
|
}
|