"use client"; import * as React from "react"; import { animate, motion, useInView, useMotionValue, useReducedMotion, useTransform } from "framer-motion"; import { IconArrowDownRight, IconArrowUpRight } from "@tabler/icons-react"; import { cn } from "../lib/utils"; import { Sparkline } from "./chart"; /** Counts up to a number once it scrolls into view. */ export function AnimatedNumber({ value, format = (next) => Math.round(next).toLocaleString(), className, }: { value: number; format?: (value: number) => string; className?: string; }) { const ref = React.useRef(null); const inView = useInView(ref, { once: true }); const reduceMotion = useReducedMotion(); const motionValue = useMotionValue(reduceMotion ? value : 0); const display = useTransform(motionValue, (latest) => format(latest)); React.useEffect(() => { if (!inView) return; if (reduceMotion) { motionValue.set(value); return; } const controls = animate(motionValue, value, { duration: 0.8, ease: [0.22, 1, 0.36, 1] }); return () => controls.stop(); }, [inView, motionValue, reduceMotion, value]); return ( {display} ); } export interface StatProps { label: string; value: React.ReactNode; delta?: { value: string; tone?: "positive" | "negative" | "neutral" }; hint?: React.ReactNode; trend?: number[]; className?: string; } /** One metric: label, value, change, and an optional trend line. */ export function Stat({ label, value, delta, hint, trend, className }: StatProps) { const tone = delta?.tone ?? "neutral"; return (

{label}

{value}
{trend && trend.length > 1 ? ( ) : null}
{delta || hint ? (
{delta ? ( {tone === "positive" ? : null} {tone === "negative" ? : null} {delta.value} ) : null} {hint ? {hint} : null}
) : null}
); } /** A responsive row of stats. */ export function StatGrid({ children, className }: { children: React.ReactNode; className?: string }) { return
{children}
; }