/* global React */
const { useState, useEffect, useRef } = React;
// ─────────────────────────────────────────────────────────────────
// SCROLL STORE — single rAF-driven scrollY source for all parallax
// ─────────────────────────────────────────────────────────────────
const scrollListeners = new Set();
let _scrollY = typeof window !== "undefined" ? window.scrollY : 0;
let _ticking = false;
function _emit() {
_scrollY = window.scrollY;
scrollListeners.forEach((fn) => fn(_scrollY));
_ticking = false;
}
if (typeof window !== "undefined") {
window.addEventListener("scroll", () => {
if (!_ticking) { _ticking = true; requestAnimationFrame(_emit); }
}, { passive: true });
}
function useScrollY() {
const [y, setY] = useState(_scrollY);
useEffect(() => {
const fn = (v) => setY(v);
scrollListeners.add(fn);
fn(window.scrollY);
return () => scrollListeners.delete(fn);
}, []);
return y;
}
// ─────────────────────────────────────────────────────────────────
// useInView — IntersectionObserver is unreliable in sandboxed iframes,
// so we drive visibility off getBoundingClientRect + the scroll store.
// ─────────────────────────────────────────────────────────────────
function useInView(ref, { margin = 0.12, fallback = 1800 } = {}) {
const [inView, setInView] = useState(false);
useEffect(() => {
let raf, settled = false;
const reveal = () => { if (!settled) { settled = true; setInView(true); } };
const check = () => {
if (settled) return;
const el = ref.current;
if (el) {
const r = el.getBoundingClientRect();
const h = window.innerHeight || document.documentElement.clientHeight;
if (r.top < h * (1 - margin) && r.bottom > 0) { reveal(); return; }
}
raf = requestAnimationFrame(check);
};
raf = requestAnimationFrame(check);
// scroll-driven check (works even if rAF is throttled)
const onScroll = () => check();
window.addEventListener("scroll", onScroll, { passive: true });
// hard safety net: never leave content permanently hidden
const t = setTimeout(reveal, fallback);
return () => { cancelAnimationFrame(raf); clearTimeout(t); window.removeEventListener("scroll", onScroll); };
}, [ref, margin, fallback]);
return inView;
}
// ─────────────────────────────────────────────────────────────────
// REVEAL — fade/slide children in when they enter the viewport
// ─────────────────────────────────────────────────────────────────
function Reveal({ children, y = 28, delay = 0, className = "", as: Tag = "div" }) {
const ref = useRef(null);
const shown = useInView(ref);
return (
{ boost.current = hoverBoost; }}
onMouseLeave={() => { boost.current = 1; }}
style={{ width: size, height: size, ...style }}
/>
);
}
// ─────────────────────────────────────────────────────────────────
// PARALLAX LAYER — translateY proportional to scroll
// ─────────────────────────────────────────────────────────────────
function ParallaxLayer({ speed = 0.2, children, className = "", style = {} }) {
const ref = useRef(null);
useEffect(() => {
const fn = (y) => {
if (ref.current) ref.current.style.transform = `translate3d(0, ${y * speed}px, 0)`;
};
scrollListeners.add(fn);
fn(window.scrollY);
return () => scrollListeners.delete(fn);
}, [speed]);
return (