// Reveal, Eyebrow, useCountUp, ScrollToTop, useHashRoute.
(function () {
const { useState, useEffect, useRef, useCallback } = React;
function Reveal({ children, delay = 0, as: Tag = 'div', className = '', style, ...rest }) {
const ref = useRef(null);
const [shown, setShown] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { setShown(true); io.disconnect(); }
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
io.observe(el);
return () => io.disconnect();
}, []);
return (
{children}
);
}
function useCountUp(target, duration = 900) {
const ref = useRef(null);
const [val, setVal] = useState(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) {
const start = performance.now();
const tick = (t) => {
const p = Math.min(1, (t - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
setVal(Math.round(eased * target));
if (p < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
io.disconnect();
}
}, { threshold: 0.5 });
io.observe(el);
return () => io.disconnect();
}, [target, duration]);
return [ref, val];
}
function Eyebrow({ children, color = 'var(--color-green)', style }) {
return (
{children}
);
}
// Simple hash router. Returns [route, setRoute].
function useHashRoute() {
const get = () => {
const h = window.location.hash.replace(/^#/, '');
return h || '/';
};
const [route, setRoute] = useState(get());
useEffect(() => {
const onHash = () => setRoute(get());
window.addEventListener('hashchange', onHash);
return () => window.removeEventListener('hashchange', onHash);
}, []);
const go = useCallback((path) => {
if (window.location.hash !== '#' + path) {
window.location.hash = path;
}
// Scroll to top on every navigation (covers same-page clicks too).
window.scrollTo({ top: 0, left: 0, behavior: 'instant' in window ? 'instant' : 'auto' });
}, []);
// Auto scroll-to-top whenever route changes.
useEffect(() => {
window.scrollTo({ top: 0, left: 0, behavior: 'instant' in window ? 'instant' : 'auto' });
}, [route]);
return [route, go];
}
// — anchor that updates hash without reload.
function Link({ to, onClick, children, ...rest }) {
const handle = (e) => {
e.preventDefault();
if (onClick) onClick(e);
window.location.hash = to;
window.scrollTo({ top: 0, left: 0, behavior: 'instant' in window ? 'instant' : 'auto' });
};
return (
{children}
);
}
window.GR = Object.assign(window.GR || {}, {
Reveal, useCountUp, Eyebrow, useHashRoute, Link,
});
})();