// Custom cursor — dot + ring, expands on interactive elements const { useEffect: useCursorEffect, useRef: useCursorRef } = React; function CustomCursor(){ const dot = useCursorRef(null); const ring = useCursorRef(null); const pos = useCursorRef({ x: -100, y: -100 }); const rpos = useCursorRef({ x: -100, y: -100 }); const raf = useCursorRef(0); useCursorEffect(() => { if(window.matchMedia('(hover: none)').matches) return; const onMove = (e) => { pos.current.x = e.clientX; pos.current.y = e.clientY; }; window.addEventListener('mousemove', onMove); const loop = () => { // dot — instant if(dot.current){ dot.current.style.transform = `translate(${pos.current.x}px, ${pos.current.y}px) translate(-50%, -50%)`; } // ring — lagged with easing rpos.current.x += (pos.current.x - rpos.current.x) * 0.18; rpos.current.y += (pos.current.y - rpos.current.y) * 0.18; if(ring.current){ ring.current.style.transform = `translate(${rpos.current.x}px, ${rpos.current.y}px) translate(-50%, -50%)`; } raf.current = requestAnimationFrame(loop); }; raf.current = requestAnimationFrame(loop); // Hover detection const checkHover = (e) => { const t = e.target; let label = null; // Walk up to find an interactive target const inter = t.closest && t.closest('a, button, input, textarea, select, label, [data-cursor]'); if(inter){ document.body.classList.add('cursor-hover'); const ds = inter.getAttribute('data-cursor'); if(ds && ds !== 'true' && ds !== ''){ document.body.classList.add('cursor-text'); label = ds; if(ring.current) ring.current.setAttribute('data-label', label); } else { document.body.classList.remove('cursor-text'); if(ring.current) ring.current.setAttribute('data-label', ''); } } else { document.body.classList.remove('cursor-hover', 'cursor-text'); if(ring.current) ring.current.setAttribute('data-label', ''); } }; window.addEventListener('mousemove', checkHover, true); const onLeave = () => { if(dot.current) dot.current.style.opacity = 0; if(ring.current) ring.current.style.opacity = 0; }; const onEnter = () => { if(dot.current) dot.current.style.opacity = 1; if(ring.current) ring.current.style.opacity = 0.7; }; document.addEventListener('mouseleave', onLeave); document.addEventListener('mouseenter', onEnter); return () => { cancelAnimationFrame(raf.current); window.removeEventListener('mousemove', onMove); window.removeEventListener('mousemove', checkHover, true); document.removeEventListener('mouseleave', onLeave); document.removeEventListener('mouseenter', onEnter); }; }, []); return (
); } window.CustomCursor = CustomCursor;