// Reveal-on-scroll primitive. Adds a fade + rise once the element enters viewport.
function Reveal({ children, delay = 0, y = 24, className = '', style = {}, as: Tag = 'div' }) {
const ref = React.useRef(null);
const [shown, setShown] = React.useState(false);
React.useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) { setShown(true); io.unobserve(el); }
});
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
io.observe(el);
return () => io.disconnect();
}, []);
return (
{children}
);
}
// Number that counts up when it enters viewport. Optional decimals and suffix/prefix.
function CountUp({ to, from = 0, decimals = 0, duration = 1400, prefix = '', suffix = '', className = '', style = {} }) {
const ref = React.useRef(null);
const [val, setVal] = React.useState(from);
const started = React.useRef(false);
React.useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting && !started.current) {
started.current = true;
const start = performance.now();
const tick = (t) => {
const p = Math.min(1, (t - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
setVal(from + (to - from) * eased);
if (p < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
});
}, { threshold: 0.3 });
io.observe(el);
return () => io.disconnect();
}, [to, from, duration]);
return (
{prefix}{val.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}{suffix}
);
}
// Animated sparkline that redraws itself
function Sparkline({ width = 120, height = 30, color = '#9387FF', data }) {
const ref = React.useRef(null);
const pts = React.useMemo(() => data || Array.from({ length: 24 }, (_, i) => 20 + Math.sin(i * 0.7) * 8 + Math.random() * 10), [data]);
const max = Math.max(...pts), min = Math.min(...pts);
const path = pts.map((v, i) => {
const x = (i / (pts.length - 1)) * width;
const y = height - ((v - min) / (max - min || 1)) * (height - 4) - 2;
return (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1);
}).join(' ');
return (
);
}
Object.assign(window, { Reveal, CountUp, Sparkline });