"use client"; import { useEffect, useRef } from "react"; import { usePathname } from "next/navigation"; import { ArrowUpRight } from "lucide-react"; const nativeTargets = 'input, textarea, select, [contenteditable="true"], canvas, video, iframe, dialog[open], [popover], .leaflet-container, [data-native-cursor], [role="slider"], button:disabled, [aria-disabled="true"]'; const interactiveTargets = 'a[href], button, summary, label[for], [role="button"]'; export function SiteCursor() { const cursorRef = useRef(null); const ringRef = useRef(null); const dotRef = useRef(null); const pathname = usePathname(); useEffect(() => { const cursor = cursorRef.current; const ring = ringRef.current; const dot = dotRef.current; if (!cursor || !ring || !dot) return; const preference = matchMedia( "(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)", ); let visible = false; let frame = 0; let previousTime = 0; let targetX = 0; let targetY = 0; let ringX = 0; let ringY = 0; const hide = () => { visible = false; cursor.dataset.visible = "false"; cursor.dataset.pressed = "false"; document.documentElement.classList.remove("has-site-cursor"); cancelAnimationFrame(frame); frame = 0; }; const paint = (time: number) => { const elapsed = Math.min(time - previousTime || 16, 40); previousTime = time; const follow = 1 - Math.exp(-elapsed / 55); ringX += (targetX - ringX) * follow; ringY += (targetY - ringY) * follow; ring.style.transform = `translate3d(${ringX}px, ${ringY}px, 0)`; dot.style.transform = `translate3d(${targetX}px, ${targetY}px, 0)`; if (Math.hypot(targetX - ringX, targetY - ringY) > 0.1) { frame = requestAnimationFrame(paint); } else { frame = 0; } }; const move = (event: PointerEvent) => { const element = event.target instanceof Element ? event.target : null; if ( !preference.matches || event.pointerType !== "mouse" || document.pointerLockElement || !element || element.closest(nativeTargets) ) { hide(); return; } targetX = event.clientX; targetY = event.clientY; cursor.dataset.mode = element.closest(interactiveTargets) ? "link" : "default"; if (!visible) { ringX = targetX; ringY = targetY; previousTime = 0; ring.style.transform = `translate3d(${ringX}px, ${ringY}px, 0)`; dot.style.transform = `translate3d(${targetX}px, ${targetY}px, 0)`; visible = true; cursor.dataset.visible = "true"; document.documentElement.classList.add("has-site-cursor"); } if (!frame) { previousTime = 0; frame = requestAnimationFrame(paint); } }; const leave = (event: PointerEvent) => { if (!event.relatedTarget) hide(); }; const press = () => { cursor.dataset.pressed = "true"; }; const release = () => { cursor.dataset.pressed = "false"; }; const keyboard = (event: KeyboardEvent) => { if (event.key === "Tab" || event.key === "Escape") hide(); }; const visibility = () => { if (document.hidden) hide(); }; document.addEventListener("pointermove", move, { passive: true }); document.addEventListener("pointerover", move, { passive: true }); document.addEventListener("pointerout", leave, { passive: true }); document.addEventListener("pointerdown", press, { passive: true }); document.addEventListener("pointerup", release, { passive: true }); document.addEventListener("pointercancel", hide); document.addEventListener("keydown", keyboard); document.addEventListener("visibilitychange", visibility); document.addEventListener("pointerlockchange", hide); window.addEventListener("blur", hide); preference.addEventListener("change", hide); return () => { hide(); document.removeEventListener("pointermove", move); document.removeEventListener("pointerover", move); document.removeEventListener("pointerout", leave); document.removeEventListener("pointerdown", press); document.removeEventListener("pointerup", release); document.removeEventListener("pointercancel", hide); document.removeEventListener("keydown", keyboard); document.removeEventListener("visibilitychange", visibility); document.removeEventListener("pointerlockchange", hide); window.removeEventListener("blur", hide); preference.removeEventListener("change", hide); }; }, [pathname]); return ( ); }