{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"stats-count","type":"registry:ui","title":"Stats Count","description":"A row of big statistics that count up with a staggered reveal when scrolled into view.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/stats-count.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface StatCountItem {\n  value: number;\n  label: string;\n  prefix?: string;\n  suffix?: string;\n  decimals?: number;\n  /** Optional smaller line under the label. */\n  description?: string;\n}\n\nexport interface StatsCountProps {\n  stats: StatCountItem[];\n  /** Ms each number takes to count up. */\n  duration?: number;\n  /** Ms between each stat starting. */\n  stagger?: number;\n  /** Show hairline dividers between stats. */\n  dividers?: boolean;\n  /** Count again every time the row re-enters the viewport. */\n  repeat?: boolean;\n  /** Color of the accent bar that sweeps under each number. */\n  accent?: string;\n  className?: string;\n}\n\nconst css = `\n.stats-count-item{opacity:0;transform:translateY(14px);filter:blur(4px);transition:opacity .7s ease,transform .8s cubic-bezier(.2,.8,.2,1),filter .7s ease;transition-delay:var(--d)}\n.stats-count-bar{transform:scaleX(0);transform-origin:left;transition:transform 1s cubic-bezier(.2,.8,.2,1);transition-delay:calc(var(--d) + .15s)}\n[data-in] .stats-count-item{opacity:1;transform:none;filter:none}\n[data-in] .stats-count-bar{transform:scaleX(1)}\n@media (prefers-reduced-motion: reduce){.stats-count-item,.stats-count-bar{transition:none}}\n`;\n\nconst fmt = (n: number, d: number) => n.toLocaleString(\"en-US\", { minimumFractionDigits: d, maximumFractionDigits: d });\n\nexport function StatsCount({\n  stats,\n  duration = 2200,\n  stagger = 140,\n  dividers = true,\n  repeat = false,\n  accent = \"#a3e635\",\n  className,\n}: StatsCountProps) {\n  const root = useRef<HTMLDListElement>(null);\n\n  useEffect(() => {\n    const el = root.current;\n    if (!el) return;\n    const nums = Array.from(el.querySelectorAll<HTMLElement>(\"[data-num]\"));\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    let raf = 0;\n\n    const reset = () => {\n      cancelAnimationFrame(raf);\n      delete el.dataset.in;\n      nums.forEach((n, i) => (n.textContent = fmt(0, stats[i]?.decimals ?? 0)));\n    };\n\n    const run = () => {\n      el.dataset.in = \"\";\n      const t0 = performance.now();\n      const tick = (now: number) => {\n        let done = true;\n        nums.forEach((n, i) => {\n          const s = stats[i];\n          if (!s) return;\n          const p = reduced ? 1 : Math.min(Math.max((now - t0 - i * stagger) / duration, 0), 1);\n          if (p < 1) done = false;\n          // Expo-out: races early, settles softly on the final digits.\n          const e = p === 1 ? 1 : 1 - Math.pow(2, -10 * p);\n          n.textContent = fmt(s.value * e, s.decimals ?? 0);\n        });\n        if (!done) raf = requestAnimationFrame(tick);\n      };\n      raf = requestAnimationFrame(tick);\n    };\n\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting) {\n          run();\n          if (!repeat) io.disconnect();\n        } else if (repeat) reset();\n      },\n      { threshold: 0.35 },\n    );\n    io.observe(el);\n    return () => {\n      io.disconnect();\n      cancelAnimationFrame(raf);\n    };\n  }, [stats, duration, stagger, repeat]);\n\n  return (\n    <dl\n      ref={root}\n      className={`grid w-full grid-cols-2 gap-y-10 md:grid-flow-col md:auto-cols-fr md:grid-cols-none ${className ?? \"\"}`}\n    >\n      <style href=\"stats-count\" precedence=\"default\">\n        {css}\n      </style>\n      {stats.map((s, i) => (\n        <div\n          key={s.label}\n          className={`stats-count-item flex min-w-0 flex-col px-5 md:px-7 ${\n            dividers && i > 0 ? \"md:border-l md:border-border\" : \"\"\n          } ${dividers && i % 2 === 1 ? \"border-l border-border\" : \"\"}`}\n          style={{ [\"--d\" as string]: `${i * stagger}ms` }}\n        >\n          <dt className=\"order-2 mt-3 text-sm font-medium text-foreground\">{s.label}</dt>\n          <dd className=\"order-1 whitespace-nowrap text-4xl font-semibold tracking-tight text-foreground sm:text-5xl\">\n            <span className=\"sr-only\">\n              {s.prefix}\n              {fmt(s.value, s.decimals ?? 0)}\n              {s.suffix}\n            </span>\n            <span aria-hidden style={{ fontVariantNumeric: \"tabular-nums\" }}>\n              {s.prefix && <span className=\"text-muted-foreground\">{s.prefix}</span>}\n              <span data-num>{fmt(0, s.decimals ?? 0)}</span>\n              {s.suffix && <span className=\"text-muted-foreground\">{s.suffix}</span>}\n            </span>\n            <span\n              aria-hidden\n              className=\"stats-count-bar mt-4 block h-px w-12\"\n              style={{ background: `linear-gradient(90deg, ${accent}, transparent)` }}\n            />\n          </dd>\n          {s.description && <dd className=\"order-3 mt-1 text-sm text-muted-foreground\">{s.description}</dd>}\n        </div>\n      ))}\n    </dl>\n  );\n}\n"}]}