"use client"; import { motion, useReducedMotion } from "motion/react"; import { useEffect, useState } from "react"; const BARS = [34, 52, 41, 68, 59, 84, 96]; const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; const W = 520; const H = 260; const PAD = 34; const CYCLE = 6; // seconds — every element loops on this beat const barWidth = (W - PAD * 2) / BARS.length - 14; const x = (i: number) => PAD + i * ((W - PAD * 2) / BARS.length) + 7; const y = (v: number) => H - PAD - (v / 100) * (H - PAD * 2); const linePath = BARS.map( (v, i) => `${i === 0 ? "M" : "L"} ${x(i) + barWidth / 2} ${y(v) - 12}`, ).join(" "); /** Counts 0 → target on the same loop as the chart. */ function Counter({ target, still }: { target: number; still: boolean }) { const [value, setValue] = useState(0); useEffect(() => { if (still) return; let frame = 0; let start: number | null = null; const tick = (now: number) => { if (start === null) start = now; const t = ((now - start) / 1000) % CYCLE; const progress = Math.min(1, t / 2.4); const eased = 1 - Math.pow(1 - progress, 3); setValue(Math.round(target * eased)); frame = requestAnimationFrame(tick); }; frame = requestAnimationFrame(tick); return () => cancelAnimationFrame(frame); }, [target, still]); return <>{(still ? target : value).toLocaleString()}>; } export default function GrowthChart() { // With reduced motion the chart snaps to its finished state and stays there. const still = useReducedMotion() ?? false; const loop = (keyframes: number[], end: number) => (still ? end : keyframes); const cycle = still ? 0 : CYCLE; return (
Annual recurring revenue
$