// scroll-reveal.jsx
// Utilities for scroll-driven animations on the landing page.
//
// useInView(ref, { threshold, once }) -> boolean
// True when the element's intersection ratio crosses threshold.
//
// useScrollProgress(ref) -> number 0..1
// Progress of the element through the viewport (0 when entering, 1 when leaving).
//
// — simple fade-up on first intersection.
const useInView = (ref, { threshold = 0.2, once = true } = {}) => {
const [inView, setInView] = React.useState(false);
React.useEffect(() => {
if (!ref.current) return;
const el = ref.current;
const obs = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setInView(true);
if (once) obs.disconnect();
} else if (!once) {
setInView(false);
}
}, { threshold, rootMargin: '0px 0px -10% 0px' });
obs.observe(el);
return () => obs.disconnect();
}, [ref, threshold, once]);
return inView;
};
// Element progress 0..1: 0 when element top hits viewport bottom; 1 when element bottom hits viewport top
const useScrollProgress = (ref) => {
const [p, setP] = React.useState(0);
React.useEffect(() => {
if (!ref.current) return;
const el = ref.current;
let raf = null;
const compute = () => {
const r = el.getBoundingClientRect();
const vh = window.innerHeight;
const total = r.height + vh;
const passed = vh - r.top;
setP(Math.max(0, Math.min(1, passed / total)));
raf = null;
};
const onScroll = () => {
if (raf) return;
raf = requestAnimationFrame(compute);
};
compute();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
return () => {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onScroll);
if (raf) cancelAnimationFrame(raf);
};
}, [ref]);
return p;
};
// Simple "enter" wrapper — fade-up once in view.
const Reveal = ({
as: Tag = 'div',
delay = 0,
y = 24,
duration = 800,
threshold = 0.2,
className = '',
style = {},
children,
...rest
}) => {
const ref = React.useRef(null);
const inView = useInView(ref, { threshold, once: true });
return (
{children}
);
};
// Run a callback repeatedly with requestAnimationFrame while `active`.
const useRaf = (callback, active = true) => {
const cbRef = React.useRef(callback);
cbRef.current = callback;
React.useEffect(() => {
if (!active) return;
let raf;
let start = performance.now();
const loop = (t) => {
cbRef.current((t - start) / 1000);
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [active]);
};
Object.assign(window, { useInView, useScrollProgress, Reveal, useRaf });