{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-stat-grid","type":"registry:component","title":"Animated Stat Grid","description":"A row of stat counters that count up together the moment they scroll into view.","dependencies":["framer-motion"],"registryDependencies":[],"files":[{"path":"components/viberdy/animated-stat-grid.tsx","content":"\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { useInView } from \"framer-motion\";\n\ntype Stat = { value: number; suffix: string; label: string };\n\nconst DEFAULT_STATS: Stat[] = [\n  { value: 53, suffix: \"+\", label: \"Components\" },\n  { value: 12000, suffix: \"+\", label: \"Downloads\" },\n  { value: 98, suffix: \"%\", label: \"Satisfaction\" },\n];\n\nfunction StatCell({ stat, duration }: { stat: Stat; duration: number }) {\n  const ref = useRef<HTMLDivElement>(null);\n  const isInView = useInView(ref, { once: true, amount: 0.7 });\n  const [display, setDisplay] = useState(0);\n\n  useEffect(() => {\n    if (!isInView) return;\n    const start = performance.now();\n    let raf = 0;\n    function tick(now: number) {\n      const t = Math.min((now - start) / 1000 / duration, 1);\n      const eased = 1 - Math.pow(1 - t, 3);\n      setDisplay(Math.round(eased * stat.value));\n      if (t < 1) raf = requestAnimationFrame(tick);\n    }\n    raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(raf);\n  }, [isInView, stat.value, duration]);\n\n  return (\n    <div ref={ref} className=\"flex flex-col items-center gap-1 rounded-xl border border-neutral-200 px-4 py-5 text-center\">\n      <span className=\"text-3xl font-black tabular-nums\">\n        {display.toLocaleString()}\n        <span className=\"text-red-500\">{stat.suffix}</span>\n      </span>\n      <span className=\"text-[11px] font-medium text-neutral-500\">{stat.label}</span>\n    </div>\n  );\n}\n\nexport function AnimatedStatGrid({ stats = DEFAULT_STATS, duration = 1.5 }: { stats?: Stat[]; duration?: number }) {\n  return (\n    <div className=\"grid grid-cols-3 gap-3\">\n      {stats.map((stat, i) => (\n        <StatCell key={stat.label + i} stat={stat} duration={duration} />\n      ))}\n    </div>\n  );\n}\n","type":"registry:component","target":"components/viberdy/animated-stat-grid.tsx"}],"meta":{"tier":"free","tags":["stats","counter","grid","scroll"],"href":"https://viberdy.dev/library/animated-stat-grid","aiPrompt":"Create a React + Tailwind + Framer Motion \"animated stat grid\" component named AnimatedStatGrid.\n\nBehavior:\n- A grid of stat cells (value + suffix + label), each rolling up from 0 to its target number the moment IT scrolls into view.\n- Each cell is its OWN subcomponent with its own useRef + useInView(once: true) and its own requestAnimationFrame tween (cubic ease-out: 1 - (1-t)^3) driving a local display value — do not share one observer/animation loop across the whole grid, since cells can enter the viewport at slightly different times depending on grid position and each needs its own independent trigger and RAF loop.\n- Cancel the animation frame on unmount/dependency change.\n- Props: stats (array of {value, suffix, label}), duration (seconds per count-up).\n\nStyle: a row of 3 (or however many) bordered cells, big black tabular-nums number with a red suffix, small muted label beneath.\n\nOutput a single self-contained \"use client\" component.","license":"https://viberdy.dev/license"}}