'use client'; import { useEffect, useRef, useState } from 'react'; const HOVER_SELECTOR = 'a, button, [role="button"], input, textarea, select, summary, [data-cursor-hover]'; export function CustomCursor() { const dotRef = useRef(null); const ringRef = useRef(null); const [enabled, setEnabled] = useState(false); useEffect(() => { // Only enable on desktop pointers (>= 900px and fine pointer) const mq = window.matchMedia('(min-width: 900px) and (pointer: fine)'); const apply = () => { const on = mq.matches; setEnabled(on); document.documentElement.classList.toggle('has-cursor', on); }; apply(); mq.addEventListener('change', apply); return () => mq.removeEventListener('change', apply); }, []); useEffect(() => { if (!enabled) return; let dotX = window.innerWidth / 2; let dotY = window.innerHeight / 2; let ringX = dotX; let ringY = dotY; let raf = 0; const onMove = (e: MouseEvent) => { dotX = e.clientX; dotY = e.clientY; }; const tick = () => { // Ring lags the dot — trailing effect. ringX += (dotX - ringX) * 0.18; ringY += (dotY - ringY) * 0.18; if (dotRef.current) { dotRef.current.style.transform = `translate3d(${dotX}px, ${dotY}px, 0) translate(-50%, -50%)`; } if (ringRef.current) { ringRef.current.style.transform = `translate3d(${ringX}px, ${ringY}px, 0) translate(-50%, -50%)`; } raf = requestAnimationFrame(tick); }; const onOver = (e: MouseEvent) => { const target = e.target as HTMLElement | null; const isHover = !!target?.closest(HOVER_SELECTOR); ringRef.current?.classList.toggle('cursor-ring--hover', isHover); dotRef.current?.classList.toggle('cursor-dot--hover', isHover); }; const onDown = () => ringRef.current?.classList.add('cursor-ring--down'); const onUp = () => ringRef.current?.classList.remove('cursor-ring--down'); const onLeave = () => { ringRef.current?.classList.add('cursor--hidden'); dotRef.current?.classList.add('cursor--hidden'); }; const onEnter = () => { ringRef.current?.classList.remove('cursor--hidden'); dotRef.current?.classList.remove('cursor--hidden'); }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseover', onOver); window.addEventListener('mousedown', onDown); window.addEventListener('mouseup', onUp); document.addEventListener('mouseleave', onLeave); document.addEventListener('mouseenter', onEnter); raf = requestAnimationFrame(tick); return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseover', onOver); window.removeEventListener('mousedown', onDown); window.removeEventListener('mouseup', onUp); document.removeEventListener('mouseleave', onLeave); document.removeEventListener('mouseenter', onEnter); cancelAnimationFrame(raf); }; }, [enabled]); if (!enabled) return null; return ( <>
); }