// shared hooks + utilities
const { useEffect, useRef, useState, useMemo, useCallback } = React;
function useInView(options = {}) {
const ref = useRef(null);
const [inView, setInView] = useState(false);
useEffect(() => {
if (!ref.current) return;
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { setInView(true); io.disconnect(); }
}, { threshold: 0.15, ...options });
io.observe(ref.current);
return () => io.disconnect();
}, []);
return [ref, inView];
}
function useReveal() {
useEffect(() => {
const els = Array.from(document.querySelectorAll('.reveal'));
const reveal = (el) => el.classList.add('in');
// If an element is already in viewport (or scrolled past it) at mount,
// reveal immediately. Browsers can restore scroll or jump to a hash before
// the IO's initial callback fires, which previously left those elements
// stuck at opacity:0.
const revealIfVisibleOrPast = (el) => {
const r = el.getBoundingClientRect();
const vh = window.innerHeight || document.documentElement.clientHeight;
// Visible now OR already scrolled past the top of the viewport
if (r.top < vh && r.bottom > -200) reveal(el);
};
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
reveal(e.target);
io.unobserve(e.target);
}
});
}, { threshold: 0.12, rootMargin: '0px 0px -60px 0px' });
const observe = () => {
els.forEach(el => {
if (el.classList.contains('in')) return;
revealIfVisibleOrPast(el);
if (!el.classList.contains('in')) io.observe(el);
});
};
observe();
// Re-check after fonts/images settle in case initial layout shifted things.
const t1 = setTimeout(observe, 250);
const t2 = setTimeout(observe, 1200);
const onLoad = () => observe();
window.addEventListener('load', onLoad);
return () => {
clearTimeout(t1);
clearTimeout(t2);
window.removeEventListener('load', onLoad);
io.disconnect();
};
}, []);
}
function useCounter(target, inView, duration = 1800) {
const [val, setVal] = useState(0);
useEffect(() => {
if (!inView) return;
let start;
let raf;
const tick = (t) => {
if (!start) start = t;
const p = Math.min((t - start) / duration, 1);
const eased = 1 - Math.pow(1 - p, 3);
setVal(target * eased);
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [inView, target, duration]);
return val;
}
// SVG icons
const Icon = {
arrow: (p) => (
),
arrowUR: (p) => (
),
check: (p) => (
),
star: (p) => (
),
globe: (p) => (
),
layers: (p) => (
),
cloud: (p) => (
),
};
window.useInView = useInView;
window.useReveal = useReveal;
window.useCounter = useCounter;
window.Icon = Icon;