// Variant B — Shared components // ---- Scroll reveal hook (uses IntersectionObserver) ------------------------ const useReveal = (options = {}) => { const ref = React.useRef(null); const [visible, setVisible] = React.useState(false); React.useEffect(() => { const el = ref.current; if (!el) return; // Find the nearest scrollable ancestor so the observer works inside // the artboard's overflow container. let root = el.parentElement; while (root) { const s = getComputedStyle(root); if (/(auto|scroll)/.test(s.overflowY) || /(auto|scroll)/.test(s.overflow)) break; root = root.parentElement; } const io = new IntersectionObserver( (entries) => { entries.forEach((e) => { if (e.isIntersecting) { setVisible(true); io.unobserve(e.target); } }); }, { root: root || null, rootMargin: options.rootMargin || "0px 0px -10% 0px", threshold: options.threshold || 0.1 } ); io.observe(el); return () => io.disconnect(); }, []); return [ref, visible]; }; const Reveal = ({ children, delay = 0, y = 24, as: Tag = "div", style, ...rest }) => { const [ref, visible] = useReveal(); return ( {children} ); }; // Marquee ticker — infinite horizontal scroll const Marquee = ({ items, speed = 30, color = B.ink, bg = B.cream, border = true }) => (
{[...items, ...items, ...items].map((t, i) => (
{t}
))}
); // ---- Floating WhatsApp FAB -------------------------------------------------- const BWhatsAppFab = () => { const [open, setOpen] = React.useState(false); const [hover, setHover] = React.useState(false); const services = [ { key: "medicina", ...CLINIC.whatsapp.medicina, icon: "stethoscope" }, { key: "odonto", ...CLINIC.whatsapp.odonto, icon: "tooth" }, { key: "lab", ...CLINIC.whatsapp.lab, icon: "flask" }, ]; const openWA = (raw, label) => { const msg = encodeURIComponent(`Hola, me gustaría agendar una cita de ${label}.`); window.open(`https://wa.me/${raw}?text=${msg}`, "_blank"); setOpen(false); }; return (
{open && (
Elija servicio
{services.map((s) => ( ))}
)} {!open && ( )}
); }; const BPhotoPh = ({ label, height = 200, bg = B.creamDark, src }) => (
{src ? ( {label} ) : ( [ {label} ] )}
); const BEyebrow = ({ children, color = B.red }) => (
— {children}
); // ---- Featured service card (promos) ---------------------------------------- const BFeaturedCard = ({ item, compact = false }) => { const [hover, setHover] = React.useState(false); const isNavy = item.accent === "navy"; const bg = isNavy ? B.navy : B.red; const onDark = true; return ( setHover(true)} onMouseLeave={() => setHover(false)} style={{ display: "block", background: bg, color: B.white, padding: compact ? 20 : 32, textDecoration: "none", position: "relative", overflow: "hidden", transform: hover ? "translateY(-4px)" : "translateY(0)", boxShadow: hover ? "0 24px 60px rgba(0,0,0,0.25)" : "0 0 0 rgba(0,0,0,0)", transition: "transform 0.35s cubic-bezier(0.2,0.8,0.2,1), box-shadow 0.35s ease", }}>
— {item.kicker}
{item.price && (
{item.price}
)}
{item.title}.

{item.copy}

Contáctanos al {item.cta}
); }; // ---- Gallery tile ----------------------------------------------------------- const BGalleryTile = ({ item, height = 200 }) => { const [hover, setHover] = React.useState(false); const isRed = item.tone === "red"; const accent = isRed ? B.red : B.navy; return (
setHover(true)} onMouseLeave={() => setHover(false)} style={{ position: "relative", overflow: "hidden", cursor: "pointer", height, background: B.creamDark, border: `1px solid ${B.border}`, }}>
{item.label}
); }; // ---- Services accordion ---------------------------------------------------- const BServicesAccordion = ({ compact = false }) => { const [open, setOpen] = React.useState("medicina"); const services = [ { key: "medicina", label: "Medicina General", icon: "stethoscope", num: "01", phone: CLINIC.whatsapp.medicina, sub: CLINIC.services.medicina.sub }, { key: "lab", label: "Laboratorio Clínico", icon: "flask", num: "02", phone: CLINIC.whatsapp.lab, sub: CLINIC.services.lab.sub }, { key: "odonto", label: "Odontología", icon: "tooth", num: "03", phone: CLINIC.whatsapp.odonto, sub: CLINIC.services.odonto.sub }, ]; return (
{services.map((s) => { const isOpen = open === s.key; const items = CLINIC.services[s.key].items; return (
{items.map((it, idx) => (
{it}
))}
); })}
); }; // ---- FAQ -------------------------------------------------------------------- const BFaq = ({ compact = false }) => { const [open, setOpen] = React.useState(0); return (
{FAQ.map((f, i) => { const isOpen = open === i; return (

{f.a}

); })}
); }; // ---- Contact form ----------------------------------------------------------- const BContactForm = () => { const [form, setForm] = React.useState({ name: "", phone: "", service: "medicina", message: "" }); const [errors, setErrors] = React.useState({}); const [sent, setSent] = React.useState(false); const [focus, setFocus] = React.useState(null); const validate = () => { const e = {}; if (!form.name.trim() || form.name.trim().length < 2) e.name = "Ingrese su nombre"; if (!/^[0-9\-\s]{7,}$/.test(form.phone)) e.phone = "Teléfono inválido"; if (!form.message.trim() || form.message.trim().length < 5) e.message = "Mensaje muy corto"; setErrors(e); return Object.keys(e).length === 0; }; const onSubmit = (e) => { e.preventDefault(); if (!validate()) return; const svc = form.service === "medicina" ? CLINIC.whatsapp.medicina : form.service === "lab" ? CLINIC.whatsapp.lab : CLINIC.whatsapp.odonto; const msg = encodeURIComponent(`Hola, soy ${form.name} (tel. ${form.phone}). Servicio: ${svc.label}.\n\n${form.message}`); window.open(`https://wa.me/${svc.raw}?text=${msg}`, "_blank"); setSent(true); }; const input = (key, ok) => ({ width: "100%", padding: "14px 0 10px", background: "transparent", border: "none", borderBottom: `1px solid ${ok === false ? B.red : (focus === key ? B.red : B.ink)}`, fontSize: 16, fontFamily: "'Inter', sans-serif", outline: "none", color: B.ink, boxSizing: "border-box", transition: "border-color 0.3s ease", }); const label = { fontSize: 11, fontWeight: 700, color: B.ink3, textTransform: "uppercase", letterSpacing: 1.8, display: "block" }; if (sent) { return (
Mensaje enviado.
Se abrió WhatsApp en una nueva ventana. Le responderemos a la brevedad.
); } return (
setFocus("name")} onBlur={() => setFocus(null)} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="Juan Pérez" /> {errors.name &&
{errors.name}
}
setFocus("phone")} onBlur={() => setFocus(null)} value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} placeholder="6000-0000" /> {errors.phone &&
{errors.phone}
}