// Shared UI primitives for the Na Escala landing page const { useState, useEffect, useLayoutEffect, useRef } = React; /* ---------- Scroll reveal ---------- Bulletproof: content is VISIBLE by default. In-view elements get a one-shot CSS entrance animation (reliable). Only elements confirmed below the fold are hidden (before paint, no flash) and revealed via observer + scroll + timeout. */ function Reveal({ children, delay = 0, y = 24, as = "div", className = "", style = {} }) { const ref = useRef(null); // mode: "load" = visible w/ entrance anim | "hidden" = below fold, waiting | "shown" = revealed on scroll const [mode, setMode] = useState("load"); useLayoutEffect(() => { const el = ref.current; if (!el) return; const vh = window.innerHeight || document.documentElement.clientHeight; const r = el.getBoundingClientRect(); const belowFold = r.top >= vh * 0.92; if (!belowFold) return; // in view (or above) → keep visible with load animation setMode("hidden"); let io; const inView = () => { const rr = el.getBoundingClientRect(); return rr.top < vh * 0.9 && rr.bottom > 0; }; const reveal = () => { setMode("shown"); cleanup(); }; const onScroll = () => { if (inView()) reveal(); }; function cleanup() { if (io) io.disconnect(); window.removeEventListener("scroll", onScroll); clearTimeout(t); } if ("IntersectionObserver" in window) { io = new IntersectionObserver( (entries) => entries.forEach((e) => { if (e.isIntersecting) reveal(); }), { threshold: 0.12, rootMargin: "0px 0px -8% 0px" } ); io.observe(el); } window.addEventListener("scroll", onScroll, { passive: true }); const t = setTimeout(reveal, 2500); // ultimate safety net return cleanup; }, []); const Tag = as; const hidden = mode === "hidden"; return ( ); } /* ---------- Eyebrow label ---------- */ function Eyebrow({ children, light = false }) { return ( {children} ); } /* ---------- Button ---------- */ function Button({ children, variant = "primary", size = "md", href = "#demo", icon = true, className = "", onClick }) { return ( {children} {icon && ( )} ); } /* ---------- Icon set (stroke, 24px) ---------- */ const Icon = ({ name, size = 24, stroke = 2 }) => { const p = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round" }; switch (name) { case "calendar": return (); case "swap": return (); case "coffee": return (); case "absence": return (); case "clock": return (); case "screen": return (); case "shield": return (); case "check": return (); case "spark": return (); case "plus": return (); case "minus": return (); case "globe": return (); case "settings": return (); case "calclock": return (); case "flag": return (); case "broadcast": return (); case "userplus": return (); case "distribute": return (); case "shuffle": return (); case "building": return (); case "play": return (); case "sliders": return (); case "mail": return (); case "whatsapp": return (); case "instagram": return (); case "card": return (); case "pix": return (); case "barcode": return (); case "lock": return (); case "bolt": return (); default: return null; } }; /* ---------- Brand logo lockup ---------- */ function Logo({ light = false }) { return ( Na Escala Na Escala ); } Object.assign(window, { Reveal, Eyebrow, Button, Icon, Logo });