agency-web/app/components/Cursor.tsx

79 lines
2.7 KiB
TypeScript

"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<HTMLDivElement>(null);
const ring = useRef<HTMLDivElement>(null);
const label = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (window.matchMedia("(pointer: coarse)").matches) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
if (!dot.current || !ring.current) return;
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);
yD(e.clientY);
xR(e.clientX);
yR(e.clientY);
};
const over = (e: Event) => {
const t = (e.target as HTMLElement).closest<HTMLElement>(
"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 = (e: Event) => {
const t = (e.target as HTMLElement).closest<HTMLElement>(
"a,button,.hoverable,[data-cursor]"
);
if (!t) return;
ring.current?.classList.remove("cursor-ring--big", "cursor-ring--label");
};
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);
};
}, []);
return (
<>
<div ref={ring} className="cursor-ring" aria-hidden="true">
<span ref={label} className="cursor-ring__label" />
</div>
<div ref={dot} className="cursor-dot" aria-hidden="true" />
</>
);
}