"use client"; /** * Custom cursor: a spring-following dot + a larger ring. * - Grows + shows a contextual label when hovering [data-cursor] targets. * - Hidden on touch / coarse pointers and when prefers-reduced-motion is set * (falls back to the native cursor, which globals.css restores). * - Built with Motion springs for buttery follow without layout thrash * (transform-only, GPU-friendly). */ import { useEffect, useState } from "react"; import { motion, useMotionValue, useSpring } from "motion/react"; export default function Cursor() { const [enabled, setEnabled] = useState(false); const [hovering, setHovering] = useState(false); const [label, setLabel] = useState(""); const x = useMotionValue(-100); const y = useMotionValue(-100); const ringX = useSpring(x, { stiffness: 350, damping: 30, mass: 0.6 }); const ringY = useSpring(y, { stiffness: 350, damping: 30, mass: 0.6 }); const dotX = useSpring(x, { stiffness: 900, damping: 40 }); const dotY = useSpring(y, { stiffness: 900, damping: 40 }); useEffect(() => { const fine = window.matchMedia("(pointer: fine)").matches; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (!fine || reduce) return; setEnabled(true); document.documentElement.classList.add("has-custom-cursor"); const move = (e: PointerEvent) => { x.set(e.clientX); y.set(e.clientY); const target = (e.target as HTMLElement)?.closest( "[data-cursor], a, button" ); if (target) { setHovering(true); setLabel(target.getAttribute("data-cursor") || ""); } else { setHovering(false); setLabel(""); } }; window.addEventListener("pointermove", move, { passive: true }); return () => { window.removeEventListener("pointermove", move); document.documentElement.classList.remove("has-custom-cursor"); }; }, [x, y]); if (!enabled) return null; return ( ); }