{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stat-cards",
  "title": "Stat Cards",
  "description": "KPI tiles with a count-up figure and a signed delta chip.",
  "registryDependencies": [
    "https://components.exe.xyz/r/chart-card.json",
    "https://components.exe.xyz/r/chip.json",
    "https://components.exe.xyz/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/blocks/stat-cards.tsx",
      "content": "\"use client\"\n\nimport type { ComponentType, ReactNode } from \"react\"\n\nimport { useCountUp } from \"@/lib/use-count-up\"\nimport { cn } from \"@/lib/utils\"\nimport { Chip } from \"@/components/ui/chip\"\n\nexport type IconComponent = ComponentType<{\n  className?: string\n  \"aria-hidden\"?: boolean\n}>\n\nfunction formatDeltaPct(delta: number) {\n  if (delta === 0) return \"0.00%\"\n  const sign = delta > 0 ? \"+\" : \"−\"\n  return `${sign}${(Math.abs(delta) * 100).toFixed(1)}%`\n}\n\nfunction deltaCopy(delta?: number) {\n  if (delta == null || delta === 0) return \"unchanged\"\n  return delta > 0\n    ? `up ${(delta * 100).toFixed(1)} percent`\n    : `down ${(Math.abs(delta) * 100).toFixed(1)} percent`\n}\n\nexport function StatCard({\n  label,\n  value,\n  format = (n) => n.toLocaleString(\"en-US\"),\n  delta,\n  icon: Icon,\n  loading = false,\n  className,\n}: {\n  label: string\n  value: number\n  format?: (n: number) => string\n  delta?: number\n  icon?: IconComponent\n  loading?: boolean\n  className?: string\n}) {\n  const counted = useCountUp(loading ? 0 : value)\n  const display = format(counted)\n  const aria = `${label}, ${format(value)}, ${deltaCopy(delta)}`\n\n  if (loading) {\n    return (\n      <article\n        data-slot=\"stat-card\"\n        aria-busy=\"true\"\n        aria-label={`${label}, loading`}\n        className={cn(\n          \"flex flex-col gap-2 rounded-[2px] border border-border bg-card px-3 py-3\",\n          className\n        )}\n      >\n        <div className=\"h-3 w-20 rounded-[2px] bg-muted\" />\n        <div className=\"h-7 w-28 rounded-[2px] bg-muted\" />\n        <div className=\"h-5 w-14 rounded-[2px] bg-muted\" />\n      </article>\n    )\n  }\n\n  return (\n    <article\n      data-slot=\"stat-card\"\n      aria-label={aria}\n      className={cn(\n        \"flex flex-col gap-1.5 rounded-[2px] border border-border bg-card px-3 py-3\",\n        className\n      )}\n    >\n      <div className=\"flex items-center justify-between gap-2\">\n        <p className=\"font-mono text-[11px] text-muted-foreground\">{label}</p>\n        {Icon ? (\n          <Icon aria-hidden className=\"size-3.5 text-muted-foreground\" />\n        ) : null}\n      </div>\n      <p className=\"font-heading text-2xl font-semibold tracking-tight tabular-nums\">\n        {display}\n      </p>\n      {delta != null ? (\n        <Chip\n          variant=\"outline\"\n          className={cn(\n            \"h-5 px-1.5 font-semibold\",\n            delta > 0 &&\n              \"border-transparent bg-[color-mix(in_srgb,var(--chart-2)_18%,transparent)] text-[var(--chart-2)]\",\n            delta < 0 &&\n              \"border-transparent bg-destructive/10 text-destructive\",\n            delta === 0 && \"text-muted-foreground\"\n          )}\n        >\n          {formatDeltaPct(delta)}\n        </Chip>\n      ) : null}\n    </article>\n  )\n}\n\nexport function StatCardRow({\n  children,\n  className,\n}: {\n  children: ReactNode\n  className?: string\n}) {\n  return (\n    <div\n      data-slot=\"stat-card-row\"\n      className={cn(\n        \"grid w-full grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4\",\n        className\n      )}\n    >\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:block"
    },
    {
      "path": "src/lib/use-count-up.ts",
      "content": "import { useEffect, useRef, useState } from \"react\"\n\nfunction prefersReducedMotion() {\n  if (typeof window === \"undefined\") return false\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n}\n\nfunction easeOutCubic(t: number) {\n  return 1 - (1 - t) ** 3\n}\n\nfunction roundTo(value: number, decimals: number) {\n  if (decimals <= 0) return Math.round(value)\n  const factor = 10 ** decimals\n  return Math.round(value * factor) / factor\n}\n\nexport function useCountUp(\n  target: number,\n  options?: { durationMs?: number; decimals?: number }\n): number {\n  const durationMs = options?.durationMs ?? 520\n  const decimals = options?.decimals ?? 0\n  const [value, setValue] = useState(0)\n  const currentRef = useRef(0)\n\n  useEffect(() => {\n    const from = currentRef.current\n\n    if (prefersReducedMotion() || durationMs <= 0) {\n      currentRef.current = target\n      setValue(target)\n      return\n    }\n\n    if (from === target) {\n      setValue(target)\n      return\n    }\n\n    const started = performance.now()\n    let frame = 0\n\n    const tick = (now: number) => {\n      const t = Math.min(1, (now - started) / durationMs)\n      const next = from + (target - from) * easeOutCubic(t)\n      if (t >= 1) {\n        currentRef.current = target\n        setValue(target)\n        return\n      }\n      currentRef.current = next\n      setValue(roundTo(next, decimals))\n      frame = requestAnimationFrame(tick)\n    }\n\n    frame = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(frame)\n  }, [target, durationMs, decimals])\n\n  return value\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:block"
}