diff --git a/app/components/Cursor.tsx b/app/components/Cursor.tsx index 6145fc4..a4c6697 100644 --- a/app/components/Cursor.tsx +++ b/app/components/Cursor.tsx @@ -1,20 +1,34 @@ "use client"; +/** + * Cursor — a character cursor with a difference-blend "lens". + * + * A small dot tracks tightly; a larger ring trails with easing and inverts the + * content beneath it (mix-blend-mode: difference). Over interactive elements it + * grows; over elements carrying data-cursor it swaps in a contextual label + * (e.g. "ver"). Disabled on touch / coarse pointers. Native cursor remains as a + * fallback so the page is always usable. + */ + import { useEffect, useRef } from "react"; import { gsap } from "gsap"; export default function Cursor() { const dot = useRef(null); const ring = useRef(null); + const label = useRef(null); useEffect(() => { if (window.matchMedia("(pointer: coarse)").matches) return; + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; if (!dot.current || !ring.current) return; - const xD = gsap.quickTo(dot.current, "x", { duration: 0.15, ease: "power3" }); - const yD = gsap.quickTo(dot.current, "y", { duration: 0.15, ease: "power3" }); - const xR = gsap.quickTo(ring.current, "x", { duration: 0.5, ease: "power3" }); - const yR = gsap.quickTo(ring.current, "y", { duration: 0.5, ease: "power3" }); + document.body.classList.add("has-custom-cursor"); + + const xD = gsap.quickTo(dot.current, "x", { duration: 0.12, ease: "power3" }); + const yD = gsap.quickTo(dot.current, "y", { duration: 0.12, ease: "power3" }); + const xR = gsap.quickTo(ring.current, "x", { duration: 0.55, ease: "power3" }); + const yR = gsap.quickTo(ring.current, "y", { duration: 0.55, ease: "power3" }); const move = (e: MouseEvent) => { xD(e.clientX); @@ -24,16 +38,30 @@ export default function Cursor() { }; const over = (e: Event) => { - if ((e.target as HTMLElement).closest("a,button,.hoverable")) { - ring.current?.classList.add("cursor-ring--big"); + const t = (e.target as HTMLElement).closest( + "a,button,.hoverable,[data-cursor]" + ); + if (!t) return; + ring.current?.classList.add("cursor-ring--big"); + const text = t.getAttribute("data-cursor"); + if (text && label.current) { + label.current.textContent = text; + ring.current?.classList.add("cursor-ring--label"); } }; - const out = () => ring.current?.classList.remove("cursor-ring--big"); + const out = (e: Event) => { + const t = (e.target as HTMLElement).closest( + "a,button,.hoverable,[data-cursor]" + ); + if (!t) return; + ring.current?.classList.remove("cursor-ring--big", "cursor-ring--label"); + }; - window.addEventListener("mousemove", move); + window.addEventListener("mousemove", move, { passive: true }); document.addEventListener("mouseover", over); document.addEventListener("mouseout", out); return () => { + document.body.classList.remove("has-custom-cursor"); window.removeEventListener("mousemove", move); document.removeEventListener("mouseover", over); document.removeEventListener("mouseout", out); @@ -42,8 +70,10 @@ export default function Cursor() { return ( <> -
-
+ +