/* ============================================================
EDITORIAL INDEX — components + hooks
============================================================ */
const { useState, useEffect, useRef, useCallback } = React;
/* scroll-driven in-view (reliable across embeds) + safety reveal */
function useInView(opts) {
const ref = useRef(null);
const [seen, setSeen] = useState(false);
const margin = (opts && opts.margin != null) ? opts.margin : 0.92;
useEffect(() => {
const el = ref.current; if (!el) return;
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setSeen(true); return; }
let done = false;
const check = () => {
if (done) return;
const r = el.getBoundingClientRect();
const vh = window.innerHeight || document.documentElement.clientHeight;
if (r.top < vh * margin && r.bottom > 0) {
done = true; setSeen(true);
window.removeEventListener("scroll", check); window.removeEventListener("resize", check);
}
};
const r1 = requestAnimationFrame(check);
const r2 = requestAnimationFrame(() => requestAnimationFrame(check));
window.addEventListener("scroll", check, { passive: true });
window.addEventListener("resize", check);
const safety = setTimeout(() => { if (!done) { done = true; setSeen(true); } }, 2600);
return () => { cancelAnimationFrame(r1); cancelAnimationFrame(r2); clearTimeout(safety);
window.removeEventListener("scroll", check); window.removeEventListener("resize", check); };
}, []);
return [ref, seen];
}
/* reveal wrapper — stamps rv-done shortly after to hard-lock end state */
function Reveal({ children, delay = 0, as = "div", className = "", style = {}, ...rest }) {
const [ref, seen] = useInView();
const [done, setDone] = useState(false);
useEffect(() => { if (!seen) return; const id = setTimeout(() => setDone(true), 1400 + delay); return () => clearTimeout(id); }, [seen, delay]);
const Tag = as;
return (
{p}
)}