/* ============================================================ Composants partagés — Site Dr Lozach Curseur, Nav, Footer, Reveal hooks, Marquee ============================================================ */ const { useState, useEffect, useRef } = React; /* ---------- Custom cursor ---------- */ function Cursor() { const dot = useRef(null); const ring = useRef(null); useEffect(() => { if (window.matchMedia('(max-width: 900px)').matches) return; let mx = window.innerWidth / 2,my = window.innerHeight / 2; let rx = mx,ry = my; const move = (e) => {mx = e.clientX;my = e.clientY;if (dot.current) {dot.current.style.left = mx + 'px';dot.current.style.top = my + 'px';}}; const tick = () => { rx += (mx - rx) * 0.18; ry += (my - ry) * 0.18; if (ring.current) {ring.current.style.left = rx + 'px';ring.current.style.top = ry + 'px';} requestAnimationFrame(tick); }; const enter = (e) => {if (ring.current && e.target.closest('a, button, .hoverable, input, textarea')) ring.current.classList.add('hover');}; const leave = (e) => {if (ring.current && e.target.closest('a, button, .hoverable, input, textarea')) ring.current.classList.remove('hover');}; window.addEventListener('mousemove', move); document.addEventListener('mouseover', enter); document.addEventListener('mouseout', leave); requestAnimationFrame(tick); return () => { window.removeEventListener('mousemove', move); document.removeEventListener('mouseover', enter); document.removeEventListener('mouseout', leave); }; }, []); return ( <>
); } /* ---------- Reveal on scroll ---------- */ function useReveal() { useEffect(() => { const els = document.querySelectorAll('.reveal, .line-mask'); const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) {e.target.classList.add('in');io.unobserve(e.target);} }); }, { threshold: 0.15, rootMargin: '0px 0px -50px 0px' }); els.forEach((el) => io.observe(el)); return () => io.disconnect(); }); } /* ---------- Nav ---------- */ function Nav({ active, dark }) { const [scrolled, setScrolled] = useState(false); const [open, setOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 20); window.addEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll); }, []); useEffect(() => { document.body.style.overflow = open ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [open]); const links = [ { href: 'index.html', label: 'Accueil', key: 'accueil' }, { href: 'pathologies.html', label: 'Pathologies', key: 'pathologies' }, { href: 'chirurgie.html', label: 'Chirurgies', key: 'chirurgie' }, { href: 'parcours.html', label: 'Parcours', key: 'parcours' }, { href: 'faq.html', label: 'FAQ', key: 'faq' }, { href: 'contact.html', label: 'Contact', key: 'contact' }, ]; return ( <> {open && (
setOpen(false)}>
e.stopPropagation()}>
setOpen(false)}> Dr François Lozach
setOpen(false)}> Prendre RDV →
)} ); } /* ---------- Floating Doctolib button ---------- */ function DoctolibFloat() { return ( Prendre RDV → ); } /* ---------- Footer ---------- */ function Footer() { return ( <> ); } /* ---------- Marquee ---------- */ function Marquee({ items, gold = true }) { const content =
{[...items, ...items, ...items].map((it, i) =>
{it} {gold && }
)}
; return
{content}
; } /* ---------- Anatomical accent (abstract joint) ---------- */ function JointGlyph({ kind = 'hip', size = 600 }) { // Abstract anatomical line drawing — never realistic, always editorial if (kind === 'hip') { return ( ); } if (kind === 'knee') { return ( ); } // foot return ( ); } /* ---------- Number ticker ---------- */ function Counter({ to, suffix = '', duration = 2000 }) { const [n, setN] = useState(0); const ref = useRef(null); useEffect(() => { const io = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { const start = performance.now(); const step = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setN(Math.floor(eased * to)); if (t < 1) requestAnimationFrame(step); }; requestAnimationFrame(step); io.disconnect(); } }, { threshold: 0.4 }); if (ref.current) io.observe(ref.current); return () => io.disconnect(); }, [to, duration]); return {n}{suffix}; } /* ---------- Magnetic button ---------- */ function Magnetic({ children, strength = 0.3 }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const onMove = (e) => { const r = el.getBoundingClientRect(); const x = e.clientX - (r.left + r.width / 2); const y = e.clientY - (r.top + r.height / 2); el.style.transform = `translate(${x * strength}px, ${y * strength}px)`; }; const onLeave = () => {el.style.transform = '';}; el.addEventListener('mousemove', onMove); el.addEventListener('mouseleave', onLeave); return () => { el.removeEventListener('mousemove', onMove); el.removeEventListener('mouseleave', onLeave); }; }, [strength]); return
{children}
; } /* ---------- Section heading with line mask ---------- */ function Heading({ eyebrow, children, className = '', tag: Tag = 'h2' }) { return ( <> {eyebrow &&
{eyebrow}
} {children} ); } Object.assign(window, { Cursor, Nav, Footer, DoctolibFloat, Marquee, JointGlyph, Counter, Magnetic, Heading, useReveal });