"use client";
import * as React from "react";
import { animate, motion, useInView, useMotionValue, useReducedMotion, useTransform } from "motion/react";
import { Quote } from "lucide-react";
import { cn } from "@/lib/utils";
export interface ProgressRing {
label: string;
/** 0–100 */
value: number;
/** Stroke colour (any CSS colour). Defaults to the theme primary. */
color?: string;
}
export interface ProgressBar {
label: string;
/** 0–100 */
value: number;
/** Text shown on the right; defaults to `${value}%`. */
display?: string;
/** Optional 0–100 "before" value drawn as a tick on the bar. */
before?: number;
}
export interface StatsProgressProps {
eyebrow?: string;
title?: string;
description?: string;
rings?: ProgressRing[];
barsTitle?: string;
bars?: ProgressBar[];
testimonial?: { quote: string; name: string; role: string; initials?: string };
className?: string;
}
const RINGS: ProgressRing[] = [
{ label: "Faster onboarding", value: 72, color: "var(--primary)" },
{ label: "Fewer tickets", value: 48, color: "#d946ef" },
{ label: "Renewal rate", value: 94, color: "#10b981" },
];
const BARS: ProgressBar[] = [
{ label: "Deploys automated", value: 92, before: 34 },
{ label: "Incidents auto-resolved", value: 67, before: 18 },
{ label: "Reviews under 1 hour", value: 81, before: 40 },
{ label: "Docs generated from code", value: 54, before: 9 },
];
const EASE = [0.22, 1, 0.36, 1] as const;
export function StatsProgress({
eyebrow = "Impact report 2026",
title = "The results speak in percentages",
description = "We surveyed 1,200 teams six months after switching. Here is what changed.",
rings = RINGS,
barsTitle = "Share of work now handled automatically",
bars = BARS,
testimonial = {
quote: "We cut our release cycle from two weeks to two days. The numbers on this page are the boring part — the calm is the real win.",
name: "Imani Brooks",
role: "VP Engineering, Northwind",
initials: "IB",
},
className,
}: StatsProgressProps) {
const reduce = useReducedMotion();
return (
<section className={cn("relative w-full overflow-x-clip bg-background py-16 sm:py-20", className)}>
<div className="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="max-w-2xl"
>
<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">{title}</h2>
<p className="mt-3 text-pretty text-muted-foreground">{description}</p>
</motion.div>
<div className="mt-10 grid gap-4 lg:grid-cols-[minmax(0,7fr)_minmax(0,5fr)]">
<div className="flex flex-col gap-4">
<div className="grid grid-cols-3 gap-2 rounded-3xl border bg-card p-4 shadow-sm sm:gap-4 sm:p-7">
{rings.map((r, i) => (
<Ring key={r.label} ring={r} delay={0.1 + i * 0.15} />
))}
</div>
<motion.figure
initial={{ opacity: 0, y: reduce ? 0 : 14 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.4 }}
transition={{ duration: 0.6, delay: 0.3, ease: EASE }}
className="relative flex-1 overflow-hidden rounded-3xl border bg-card p-6 shadow-sm sm:p-7"
>
<Quote aria-hidden className="absolute right-5 top-5 size-10 text-primary/15" fill="currentColor" strokeWidth={0} />
<blockquote className="relative pr-10 text-pretty text-base leading-relaxed text-card-foreground sm:text-lg">
“{testimonial.quote}”
</blockquote>
<figcaption className="mt-5 flex items-center gap-3">
<span className="flex size-10 items-center justify-center rounded-full bg-gradient-to-br from-primary to-fuchsia-500 text-sm font-semibold text-white">
{testimonial.initials ?? testimonial.name.split(" ").map((w) => w[0]).join("").slice(0, 2)}
</span>
<span>
<span className="block text-sm font-semibold text-card-foreground">{testimonial.name}</span>
<span className="block text-xs text-muted-foreground">{testimonial.role}</span>
</span>
</figcaption>
</motion.figure>
</div>
<div className="rounded-3xl border bg-card p-6 shadow-sm sm:p-7">
<h3 className="text-sm font-semibold text-card-foreground">{barsTitle}</h3>
<ul className="mt-6 space-y-6">
{bars.map((b, i) => (
<Bar key={b.label} bar={b} delay={0.15 + i * 0.12} />
))}
</ul>
<div className="mt-8 flex items-center justify-between border-t pt-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1.5">
<span className="h-2.5 w-[3px] rounded-full bg-foreground/70" /> Before switching
</span>
<span className="flex items-center gap-1.5">
<span className="h-1.5 w-4 rounded-full bg-gradient-to-r from-primary to-fuchsia-500" /> After 6 months
</span>
</div>
</div>
</div>
</div>
</section>
);
}
function useCountUp(target: number, play: boolean, delay: number, duration = 1.6) {
const reduce = useReducedMotion();
const mv = useMotionValue(0);
React.useEffect(() => {
if (!play) return;
if (reduce) {
mv.set(target);
return;
}
const c = animate(mv, target, { duration, delay, ease: [0.16, 1, 0.3, 1] });
return () => c.stop();
}, [play, target, delay, duration, reduce, mv]);
return mv;
}
function Ring({ ring, delay }: { ring: ProgressRing; delay: number }) {
const ref = React.useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once: true, amount: 0.6 });
const mv = useCountUp(ring.value, inView, delay);
const pct = useTransform(mv, (v) => Math.round(v));
const length = useTransform(mv, (v) => Math.max(0.001, v / 100));
const color = ring.color ?? "var(--primary)";
return (
<div ref={ref} className="flex min-w-0 flex-col items-center text-center">
<div className="relative aspect-square w-full max-w-[140px]">
<svg viewBox="0 0 100 100" className="size-full -rotate-90" aria-hidden>
<circle cx="50" cy="50" r="42" fill="none" stroke="var(--muted)" strokeWidth="8" />
<motion.circle
cx="50"
cy="50"
r="42"
fill="none"
stroke={color}
strokeWidth="8"
strokeLinecap="round"
style={{ pathLength: length }}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xl font-semibold tabular-nums tracking-tight text-card-foreground sm:text-3xl">
<motion.span>{pct}</motion.span>
<span className="text-sm text-muted-foreground sm:text-base">%</span>
</span>
</div>
</div>
<p className="mt-3 text-xs font-medium text-muted-foreground sm:text-sm">{ring.label}</p>
<span className="sr-only">
{ring.label}: {ring.value}%
</span>
</div>
);
}
function Bar({ bar, delay }: { bar: ProgressBar; delay: number }) {
const ref = React.useRef<HTMLLIElement>(null);
const inView = useInView(ref, { once: true, amount: 0.8 });
const mv = useCountUp(bar.value, inView, delay, 1.4);
const text = useTransform(mv, (v) => `${Math.round(v)}%`);
const width = useTransform(mv, (v) => `${v}%`);
return (
<li ref={ref}>
<div className="flex items-baseline justify-between gap-4 text-sm">
<span className="truncate text-card-foreground">{bar.label}</span>
<span className="shrink-0 font-mono text-xs tabular-nums text-muted-foreground">
{bar.display ?? <motion.span>{text}</motion.span>}
</span>
</div>
<div
className="relative mt-2 h-2.5 overflow-hidden rounded-full bg-muted"
role="progressbar"
aria-label={bar.label}
aria-valuenow={bar.value}
aria-valuemin={0}
aria-valuemax={100}
>
<motion.span
className="absolute inset-y-0 left-0 overflow-hidden rounded-full bg-gradient-to-r from-primary to-fuchsia-500"
style={{ width }}
>
<motion.span
aria-hidden
className="absolute inset-y-0 w-10 bg-gradient-to-r from-transparent via-white/40 to-transparent"
initial={{ left: "-20%" }}
animate={inView ? { left: "120%" } : undefined}
transition={{ delay: delay + 1.2, duration: 1.1, ease: "easeInOut" }}
/>
</motion.span>
{bar.before != null && (
<span
aria-hidden
className="absolute inset-y-0 w-[3px] -translate-x-1/2 rounded-full bg-foreground/70 ring-2 ring-card"
style={{ left: `${bar.before}%` }}
/>
)}
</div>
</li>
);
}