/* Shared utilities, hooks, and primitives for all 3 LP variants */
/* --- Scroll reveal hook --- */
function useReveal(threshold = 0.15) {
const ref = React.useRef(null);
const [shown, setShown] = React.useState(false);
React.useEffect(() => {
if (!ref.current) return;
const io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
setShown(true);
io.unobserve(e.target);
}
});
},
{ threshold, root: null, rootMargin: '0px 0px -10% 0px' }
);
io.observe(ref.current);
return () => io.disconnect();
}, [threshold]);
return [ref, shown];
}
/* --- Scroll progress (0..1 within element) --- */
function useScrollProgress(scrollerRef) {
const ref = React.useRef(null);
const [p, setP] = React.useState(0);
React.useEffect(() => {
const scroller = scrollerRef?.current;
if (!ref.current || !scroller) return;
const el = ref.current;
function update() {
const rect = el.getBoundingClientRect();
const scrollerRect = scroller.getBoundingClientRect();
const start = scrollerRect.top + scrollerRect.height; // when top of el reaches bottom of scroller
const top = rect.top; // relative to viewport (which is scroller in this case for the iframe)
// simpler: use scroller scrollTop relative to element offsetTop
const offsetTop = el.offsetTop;
const scrollerH = scroller.clientHeight;
const scrollTop = scroller.scrollTop;
const elH = el.offsetHeight;
const startAt = offsetTop - scrollerH;
const endAt = offsetTop + elH;
const raw = (scrollTop - startAt) / (endAt - startAt);
setP(Math.max(0, Math.min(1, raw)));
}
update();
scroller.addEventListener('scroll', update, { passive: true });
window.addEventListener('resize', update);
return () => {
scroller.removeEventListener('scroll', update);
window.removeEventListener('resize', update);
};
}, [scrollerRef]);
return [ref, p];
}
/* --- Count up on reveal --- */
function CountUp({ to, duration = 1200, suffix = '' }) {
const [ref, shown] = useReveal(0.4);
const [n, setN] = React.useState(0);
React.useEffect(() => {
if (!shown) return;
let raf;
const start = performance.now();
const tick = (t) => {
const p = Math.min(1, (t - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
setN(Math.round(to * eased));
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [shown, to, duration]);
return {n}{suffix};
}
/* --- Reveal wrapper --- */
function Reveal({ children, delay = 0, y = 24, className = '', as: As = 'div' }) {
const [ref, shown] = useReveal();
return (