// cursor.jsx — custom 2-dot cursor with smooth lerp + accent color trail function Cursor() { React.useEffect(() => { // Hide on touch devices if (window.matchMedia('(hover: none)').matches) return; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; const dot = document.createElement('div'); const ring = document.createElement('div'); dot.className = 'cursor-dot'; ring.className = 'cursor-ring'; document.body.appendChild(dot); document.body.appendChild(ring); document.documentElement.classList.add('has-cursor'); // Trail dots — chain of particles that lag behind the cursor const TRAIL = 14; const trailEls = []; const trailPos = []; for (let i = 0; i < TRAIL; i++) { const el = document.createElement('div'); el.className = 'cursor-trail'; document.body.appendChild(el); trailEls.push(el); trailPos.push({ x: 0, y: 0 }); } // Mouse target let mx = window.innerWidth / 2; let my = window.innerHeight / 2; // Ring position (lerped behind) let rx = mx, ry = my; // Dot position (lerped slightly) let dx = mx, dy = my; let visible = false; const onMove = (e) => { mx = e.clientX; my = e.clientY; if (!visible) { rx = mx; ry = my; dx = mx; dy = my; trailPos.forEach(p => { p.x = mx; p.y = my; }); dot.classList.add('on'); ring.classList.add('on'); trailEls.forEach(el => el.classList.add('on')); visible = true; } }; const onEnter = () => { if (visible) { dot.classList.add('on'); ring.classList.add('on'); trailEls.forEach(el => el.classList.add('on')); } }; const onLeave = () => { dot.classList.remove('on'); ring.classList.remove('on'); trailEls.forEach(el => el.classList.remove('on')); }; // Hover state const HOVER_SEL = 'a, button, [role="button"], .nav-link, .work-card, .cap, .svc-row, .faq-item, .budget-chip, [data-cursor="hover"]'; const onOver = (e) => { if (e.target.closest && e.target.closest(HOVER_SEL)) { ring.classList.add('hover'); dot.classList.add('hover'); } }; const onOut = (e) => { const to = e.relatedTarget; if (!to || !to.closest || !to.closest(HOVER_SEL)) { ring.classList.remove('hover'); dot.classList.remove('hover'); } }; // Click pulse const onDown = () => { ring.classList.add('down'); dot.classList.add('down'); }; const onUp = () => { ring.classList.remove('down'); dot.classList.remove('down'); }; window.addEventListener('mousemove', onMove, { passive: true }); document.addEventListener('mouseenter', onEnter); document.addEventListener('mouseleave', onLeave); document.addEventListener('mouseover', onOver); document.addEventListener('mouseout', onOut); window.addEventListener('mousedown', onDown); window.addEventListener('mouseup', onUp); let raf; const tick = () => { // Main cursor lerp rx += (mx - rx) * 0.16; ry += (my - ry) * 0.16; dx += (mx - dx) * 0.55; dy += (my - dy) * 0.55; ring.style.transform = `translate3d(${rx}px, ${ry}px, 0) translate(-50%, -50%)`; dot.style.transform = `translate3d(${dx}px, ${dy}px, 0) translate(-50%, -50%)`; // Trail: each particle chases the one in front of it (chain lerp) const LERP = 0.28; trailPos.forEach((p, i) => { const tx = i === 0 ? mx : trailPos[i - 1].x; const ty = i === 0 ? my : trailPos[i - 1].y; p.x += (tx - p.x) * LERP; p.y += (ty - p.y) * LERP; const t = 1 - i / TRAIL; // 1 at head → 0 at tail const size = Math.max(1, 5 * t); // shrinks toward tail const el = trailEls[i]; el.style.transform = `translate3d(${p.x}px, ${p.y}px, 0) translate(-50%, -50%)`; el.style.opacity = visible ? (t * 0.55).toFixed(3) : '0'; el.style.width = el.style.height = size + 'px'; }); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); window.removeEventListener('mousemove', onMove); document.removeEventListener('mouseenter', onEnter); document.removeEventListener('mouseleave', onLeave); document.removeEventListener('mouseover', onOver); document.removeEventListener('mouseout', onOut); window.removeEventListener('mousedown', onDown); window.removeEventListener('mouseup', onUp); dot.remove(); ring.remove(); trailEls.forEach(el => el.remove()); document.documentElement.classList.remove('has-cursor'); }; }, []); return null; } window.Cursor = Cursor;