/* components.jsx — shared primitives, icons, neutral mockups */
const { useState, useEffect, useRef, useCallback } = React;
/* ---------- Scroll reveal ---------- */
function Reveal({ children, delay = 0, as = "div", className = "", once = false, style, ...rest }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) { el.style.opacity = "1"; el.style.transform = "none"; return; }
const cs = getComputedStyle(el);
const vRx = parseFloat(cs.getPropertyValue("--rx"));
const vRy = cs.getPropertyValue("--ry").trim();
const RX = isNaN(vRx) ? 0 : vRx;
const RY = vRy === "" ? 30 : parseFloat(vRy) || 0;
const easeOut = (p) => 1 - Math.pow(1 - p, 3);
let raf = 0, t1 = 0, t2 = 0, poll = 0, state = "";
const clearAll = () => { if (raf) cancelAnimationFrame(raf); raf = 0; clearTimeout(t1); clearTimeout(t2); };
const setHidden = () => { el.style.opacity = "0"; el.style.transform = "translate3d(" + RX + "px," + RY + "px,0)"; };
const finalize = () => { if (raf) cancelAnimationFrame(raf); raf = 0; el.style.opacity = "1"; el.style.transform = "none"; if (poll) { clearInterval(poll); poll = 0; } };
const animateIn = () => {
if (state === "in") return;
state = "in";
clearAll();
setHidden();
let start = null;
const run = (now) => {
if (start == null) start = now;
const p = Math.min((now - start) / 720, 1);
const e = easeOut(p);
el.style.opacity = String(e);
el.style.transform = "translate3d(" + RX * (1 - e) + "px," + RY * (1 - e) + "px,0)";
if (p < 1) raf = requestAnimationFrame(run); else finalize();
};
// stagger, then animate
t1 = setTimeout(() => { raf = requestAnimationFrame(run); }, delay);
// SAFETY: timers fire even when rAF/animations are throttled/frozen —
// guarantees the element reaches its visible end-state regardless.
t2 = setTimeout(finalize, delay + 820);
};
const animateOut = (dir) => {
if (once || state === "out") return;
state = "out";
clearAll();
const oy = dir < 0 ? -14 : 14;
const from = parseFloat(el.style.opacity || "1");
const from0 = isNaN(from) ? 1 : from;
let start = null;
const run = (now) => {
if (start == null) start = now;
const p = Math.min((now - start) / 400, 1);
const e = easeOut(p);
el.style.opacity = String(from0 * (1 - e));
el.style.transform = "translate3d(0," + oy * e + "px,0)";
if (p < 1) raf = requestAnimationFrame(run); else el.style.opacity = "0";
};
raf = requestAnimationFrame(run);
// a frozen exit harmlessly leaves the element visible — no forced hide
};
const evaluate = () => {
const vh = window.innerHeight || 0;
const r = el.getBoundingClientRect();
const visible = r.top < vh * 0.9 && r.bottom > vh * 0.12;
if (visible) animateIn();
else if (state === "in") {
if (r.bottom <= vh * 0.12) animateOut(-1);
else if (r.top >= vh * 0.9) animateOut(1);
}
};
const io = new IntersectionObserver(() => evaluate(), { threshold: [0, 0.12, 0.5, 0.9] });
io.observe(el);
// Call evaluate() directly (NOT wrapped in rAF, which is throttled when the
// frame isn't painting) so scroll always re-evaluates.
const onScroll = () => evaluate();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
const its = [setTimeout(evaluate, 60), setTimeout(evaluate, 300), setTimeout(evaluate, 700)];
// low-frequency timer poll: guarantees state updates even if scroll/IO/rAF
// are all throttled in a given environment. Stops once the element settles
// visible (scroll + IO then handle any later exit/re-entry).
poll = setInterval(evaluate, 400);
// ultimate safety: never leave an on-screen element hidden
const safe = setTimeout(() => {
const r = el.getBoundingClientRect();
if (state !== "in" && r.top < (window.innerHeight || 0) && r.bottom > 0) finalize();
}, 2400);
evaluate();
return () => {
io.disconnect();
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
if (raf) cancelAnimationFrame(raf);
clearInterval(poll);
clearTimeout(t1); clearTimeout(t2); clearTimeout(safe);
its.forEach(clearTimeout);
};
}, [once, delay]);
const Tag = as;
return (
{note}
}