"use client";
import * as React from "react";
import { animate, motion, useInView, useMotionValue, useReducedMotion, useTransform } from "motion/react";
import { TrendingDown, TrendingUp } from "lucide-react";
import { cn } from "@/lib/utils";
export interface StatMetric {
label: string;
value: number;
decimals?: number;
prefix?: string;
suffix?: string;
/** e.g. "+18%" — a leading "-" or "−" renders the delta as negative. */
delta?: string;
/** Points for the sparkline (any scale). */
trend?: number[];
/** Hint under the number. */
note?: string;
}
export interface StatsCountupProps {
eyebrow?: string;
title?: React.ReactNode;
caption?: string;
footnote?: string;
metrics?: StatMetric[];
/** Count-up duration in seconds. */
duration?: number;
className?: string;
}
const METRICS: StatMetric[] = [
{
label: "Payments processed",
value: 2.4,
decimals: 1,
prefix: "$",
suffix: "B",
delta: "+38%",
note: "in the last 12 months",
trend: [8, 10, 9, 13, 12, 15, 17, 16, 20, 23, 22, 27],
},
{
label: "Uptime",
value: 99.99,
decimals: 2,
suffix: "%",
delta: "+0.02%",
note: "rolling 90-day SLA",
trend: [96, 97, 96.5, 98, 97.5, 98.6, 99, 98.8, 99.4, 99.6, 99.9, 99.99],
},
{
label: "Active teams",
value: 18400,
suffix: "+",
delta: "+2.1k",
note: "across 140 countries",
trend: [4, 5, 7, 6, 8, 10, 11, 13, 12, 15, 16, 18],
},
{
label: "Median response",
value: 38,
suffix: "ms",
delta: "−12ms",
note: "p50 API latency",
trend: [70, 66, 64, 60, 58, 55, 50, 49, 45, 43, 40, 38],
},
];
const EASE = [0.22, 1, 0.36, 1] as const;
export function StatsCountup({
eyebrow = "By the numbers",
title = (
<>
Infrastructure trusted with{" "}
<span className="bg-gradient-to-r from-primary to-fuchsia-500 bg-clip-text text-transparent">real money</span>.
</>
),
caption = "From two-person startups to public companies, teams run their most critical flows on us — and the numbers keep climbing.",
footnote = "Figures as of September 2026. Updated monthly.",
metrics = METRICS,
duration = 2,
className,
}: StatsCountupProps) {
const reduce = useReducedMotion();
return (
<section className={cn("relative w-full overflow-x-clip bg-background py-16 sm:py-24", className)}>
<div aria-hidden className="pointer-events-none absolute inset-x-0 top-0 h-full">
<div className="absolute left-1/2 top-0 h-64 w-[min(900px,100%)] -translate-x-1/2 rounded-full bg-primary/10 blur-3xl" />
</div>
<div className="relative mx-auto max-w-6xl px-4 sm:px-6">
<motion.div
initial={{ opacity: 0, y: reduce ? 0 : 14 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.5 }}
transition={{ duration: 0.6, ease: EASE }}
className="grid gap-4 lg:grid-cols-2 lg:items-end lg:gap-16"
>
<div>
<p className="text-sm font-medium text-primary">{eyebrow}</p>
<h2 className="mt-2 text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-4xl lg:text-5xl lg:leading-[1.05]">
{title}
</h2>
</div>
<p className="max-w-md text-pretty text-muted-foreground lg:justify-self-end">{caption}</p>
</motion.div>
<div className="mt-12 grid gap-px overflow-hidden rounded-3xl border bg-border shadow-sm sm:grid-cols-2 lg:grid-cols-4">
{metrics.map((m, i) => (
<Metric key={m.label} metric={m} index={i} duration={duration} />
))}
</div>
{footnote && <p className="mt-4 text-center text-xs text-muted-foreground lg:text-right">{footnote}</p>}
</div>
</section>
);
}
function Metric({ metric, index, duration }: { metric: StatMetric; index: number; duration: number }) {
const ref = React.useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once: true, amount: 0.5 });
const reduce = useReducedMotion();
const mv = useMotionValue(0);
const decimals = metric.decimals ?? 0;
const fmt = React.useMemo(
() => new Intl.NumberFormat("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }),
[decimals],
);
const text = useTransform(mv, (v) => fmt.format(v));
const delay = 0.15 + index * 0.12;
React.useEffect(() => {
if (!inView) return;
if (reduce) {
mv.set(metric.value);
return;
}
const c = animate(mv, metric.value, { duration, delay, ease: [0.16, 1, 0.3, 1] });
return () => c.stop();
}, [inView, metric.value, duration, delay, reduce, mv]);
const negative = /^[-−]/.test(metric.delta ?? "");
// "Good" direction: a falling latency is positive news, so colour by the trend's direction, not the sign.
const trend = metric.trend ?? [];
const improving = trend.length > 1 ? (negative ? trend[trend.length - 1] < trend[0] : true) : !negative;
return (
<div ref={ref} className="relative bg-card">
<motion.div
initial={{ opacity: 0, y: reduce ? 0 : 16 }}
animate={inView ? { opacity: 1, y: 0 } : undefined}
transition={{ duration: 0.6, delay: index * 0.08, ease: EASE }}
className="flex h-full flex-col p-6 sm:p-7"
>
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">{metric.label}</p>
<p className="mt-3 flex items-baseline text-4xl font-semibold tracking-tight text-card-foreground tabular-nums sm:text-[2.75rem]">
{metric.prefix}
<motion.span>{text}</motion.span>
{metric.suffix && <span className="ml-0.5 text-2xl text-muted-foreground sm:text-3xl">{metric.suffix}</span>}
</p>
<div className="mt-2 flex items-center gap-2 text-xs">
{metric.delta && (
<span
className={cn(
"inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 font-medium",
improving ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" : "bg-rose-500/10 text-rose-600 dark:text-rose-400",
)}
>
{negative ? <TrendingDown className="size-3" aria-hidden /> : <TrendingUp className="size-3" aria-hidden />}
{metric.delta}
</span>
)}
{metric.note && <span className="truncate text-muted-foreground">{metric.note}</span>}
</div>
{trend.length > 1 && <Sparkline points={trend} play={inView} delay={delay + 0.2} />}
</motion.div>
</div>
);
}
function Sparkline({ points, play, delay }: { points: number[]; play: boolean; delay: number }) {
const id = `spark-${React.useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
const w = 200;
const h = 48;
const min = Math.min(...points);
const max = Math.max(...points);
const span = max - min || 1;
const xy = points.map((p, i) => [(i / (points.length - 1)) * w, 4 + (1 - (p - min) / span) * (h - 8)] as const);
const line = xy.map(([x, y], i) => `${i ? "L" : "M"}${x.toFixed(1)} ${y.toFixed(1)}`).join(" ");
const area = `${line} L${w} ${h} L0 ${h} Z`;
const [lx, ly] = xy[xy.length - 1];
return (
<div className="relative mt-6 h-12 w-full" aria-hidden>
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" className="absolute inset-0 size-full overflow-visible">
<defs>
<linearGradient id={`${id}-fill`} x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="var(--primary)" stopOpacity="0.25" />
<stop offset="100%" stopColor="var(--primary)" stopOpacity="0" />
</linearGradient>
<clipPath id={id}>
<motion.rect
x="-4"
y="-8"
height={h + 16}
initial={{ width: 0 }}
animate={{ width: play ? w + 8 : 0 }}
transition={{ duration: 1.3, delay, ease: "easeInOut" }}
/>
</clipPath>
</defs>
<g clipPath={`url(#${id})`}>
<path d={area} fill={`url(#${id}-fill)`} />
<path
d={line}
fill="none"
stroke="var(--primary)"
strokeWidth="2"
strokeLinejoin="round"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
</g>
</svg>
<motion.span
className="absolute size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary ring-4 ring-primary/20"
style={{ left: `${(lx / w) * 100}%`, top: `${(ly / h) * 100}%` }}
initial={{ scale: 0 }}
animate={{ scale: play ? 1 : 0 }}
transition={{ delay: delay + 1.25, type: "spring", stiffness: 500, damping: 20 }}
/>
</div>
);
}