// CustomCursor — gold dot + ring that follow the mouse. // Hidden on touch / coarse-pointer devices. // StickyCTA — "Prendre rendez-vous" floating button after hero scrolls past. function CustomCursor() { const dotRef = React.useRef(null); const ringRef = React.useRef(null); const [enabled, setEnabled] = React.useState(false); React.useEffect(() => { const hasFine = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches; if (!hasFine) return; setEnabled(true); let mouseX = window.innerWidth / 2; let mouseY = window.innerHeight / 2; let ringX = mouseX, ringY = mouseY; let rafId; const onMove = (e) => { mouseX = e.clientX; mouseY = e.clientY; if (dotRef.current) { dotRef.current.style.transform = `translate3d(${mouseX}px, ${mouseY}px, 0) translate(-50%, -50%)`; } }; const tick = () => { // Ring lerps to follow with elegant lag ringX += (mouseX - ringX) * 0.18; ringY += (mouseY - ringY) * 0.18; if (ringRef.current) { ringRef.current.style.transform = `translate3d(${ringX}px, ${ringY}px, 0) translate(-50%, -50%)`; } rafId = requestAnimationFrame(tick); }; tick(); const onOver = (e) => { const el = e.target; if (!el || !el.closest) return; const interactive = el.closest('a, button, [role="button"], input, textarea, select, label'); if (ringRef.current) { ringRef.current.classList.toggle('dch-cursor-active', !!interactive); } if (dotRef.current) { dotRef.current.classList.toggle('dch-cursor-active', !!interactive); } }; const onLeave = () => { if (ringRef.current) ringRef.current.style.opacity = '0'; if (dotRef.current) dotRef.current.style.opacity = '0'; }; const onEnter = () => { if (ringRef.current) ringRef.current.style.opacity = '1'; if (dotRef.current) dotRef.current.style.opacity = '1'; }; window.addEventListener('mousemove', onMove, { passive: true }); document.addEventListener('mouseover', onOver, { passive: true }); document.addEventListener('mouseleave', onLeave); document.addEventListener('mouseenter', onEnter); document.documentElement.classList.add('dch-cursor-on'); return () => { cancelAnimationFrame(rafId); window.removeEventListener('mousemove', onMove); document.removeEventListener('mouseover', onOver); document.removeEventListener('mouseleave', onLeave); document.removeEventListener('mouseenter', onEnter); document.documentElement.classList.remove('dch-cursor-on'); }; }, []); if (!enabled) return null; return ( <>
> ); } function StickyCTA() { const [visible, setVisible] = React.useState(false); const isMobile = window.__dch_isMobile; React.useEffect(() => { const onScroll = () => { // show after we're roughly past the hero setVisible(window.scrollY > 500); }; window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); return () => window.removeEventListener('scroll', onScroll); }, []); return ( Prendre rendez-vous