// 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 (
{children}
);
}
/* ---------- 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
);
}
Object.assign(window, { Reveal, Eyebrow, Button, Icon, Logo });