1"use client";
2
3import React, { useEffect, useRef, useState } from "react";
4
5interface StatProps {
6 value: number | string;
7 label: string;
8 prefix?: string;
9 suffix?: string;
10 trend?: string;
11 className?: string;
12}
13
14function useCountUp(target: number, duration = 800) {
15 const [count, setCount] = useState(0);
16 const raf = useRef<number | null>(null);
17
18 useEffect(() => {
19 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
20 raf.current = requestAnimationFrame(() => setCount(target));
21 return () => { if (raf.current) cancelAnimationFrame(raf.current); };
22 }
23 let start: number | null = null;
24 const step = (now: number) => {
25 if (!start) start = now;
26 const p = Math.min((now - start) / duration, 1);
27 setCount(Math.round((1 - Math.pow(1 - p, 3)) * target));
28 if (p < 1) raf.current = requestAnimationFrame(step);
29 };
30 raf.current = requestAnimationFrame(step);
31 return () => { if (raf.current) cancelAnimationFrame(raf.current); };
32 }, [target, duration]);
33
34 return count;
35}
36
37export function Stat({ value, label, prefix = "", suffix = "", trend, className = "" }: StatProps) {
38 const isNum = typeof value === "number";
39 const counted = useCountUp(isNum ? (value as number) : 0);
40 const displayed = isNum ? counted : value;
41
42 return (
43 <div className={["flex flex-col gap-0.5", className].join(" ")}>
44 <div className="flex items-baseline gap-1.5">
45 <span
46 className="text-[32px] font-semibold text-neutral-900 leading-none tabular-nums"
47 style={{ letterSpacing: "-0.03em" }}
48 >
49 {prefix}{displayed}{suffix}
50 </span>
51 {trend && (
52 <span className="text-[12px] text-neutral-500 font-medium">{trend}</span>
53 )}
54 </div>
55 <span className="text-[12px] text-neutral-500">{label}</span>
56 </div>
57 );
58}
59
60interface StatRowProps {
61 children: React.ReactNode;
62 className?: string;
63}
64
65export function StatRow({ children, className = "" }: StatRowProps) {
66 const arr = React.Children.toArray(children);
67 return (
68 <div className={["grid grid-cols-2 gap-6 sm:flex sm:divide-x sm:divide-neutral-200 sm:gap-0", className].join(" ")}>
69 {arr.map((child, i) => (
70 <div
71 key={i}
72 className={[
73 "min-w-0",
74 i === 0 ? "sm:pr-6" : i === arr.length - 1 ? "sm:pl-6" : "sm:px-6",
75 ].join(" ")}
76 >
77 {child}
78 </div>
79 ))}
80 </div>
81 );
82}