{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "animated-number",
  "title": "Animated Number",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/animated-number/AnimatedNumber.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\n/**\n * Tweened number display. Counts up on mount, then smoothly retargets\n * from the currently displayed value whenever `value` changes.\n * Server render outputs the final value so hydration always matches.\n */\nconst props = withDefaults(\n  defineProps<{\n    value: number\n    /** Value the first animation starts from. */\n    from?: number\n    /** ms per tween. */\n    duration?: number\n    /** ms before the first tween starts. */\n    delay?: number\n    format?: (value: number) => string\n    /** Render the target value instantly, no tween. */\n    disabled?: boolean\n    class?: HTMLAttributes['class']\n  }>(),\n  {\n    from: 0,\n    duration: 900,\n    delay: 0,\n    format: undefined,\n    disabled: false,\n  },\n)\n\nconst defaultFormat = (v: number) => String(Math.round(v))\n\nconst display = ref(props.value)\n\nlet raf = 0\nlet startTimer: number | undefined\n\nfunction cancel() {\n  if (raf) cancelAnimationFrame(raf)\n  raf = 0\n  if (startTimer !== undefined) {\n    clearTimeout(startTimer)\n    startTimer = undefined\n  }\n}\n\nfunction tweenTo(target: number, animateFrom: number, withDelay: boolean) {\n  cancel()\n  const reduce = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n  if (props.disabled || reduce) {\n    display.value = target\n    return\n  }\n  const startTime = performance.now()\n  const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3)\n  const run = () => {\n    const t = Math.min(1, (performance.now() - startTime) / props.duration)\n    display.value = animateFrom + (target - animateFrom) * easeOutCubic(t)\n    raf = t < 1 ? requestAnimationFrame(run) : 0\n  }\n  if (withDelay && props.delay > 0) {\n    startTimer = window.setTimeout(() => {\n      startTimer = undefined\n      run()\n    }, props.delay)\n  } else {\n    run()\n  }\n}\n\nonMounted(() => {\n  // First paint showed the final value (SSR-safe); snap to `from`, then animate.\n  tweenTo(props.value, props.from, true)\n})\n\nwatch(\n  () => props.value,\n  (next) => {\n    // Retarget smoothly from whatever is on screen right now.\n    tweenTo(next, display.value, false)\n  },\n)\n\nonBeforeUnmount(cancel)\n\nconst formatted = computed(() => (props.format ?? defaultFormat)(display.value))\n</script>\n\n<template>\n  <span data-uipkge data-slot=\"animated-number\" :class=\"cn('tabular-nums', props.class)\">{{ formatted }}</span>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/animated-number/AnimatedNumber.vue"
    },
    {
      "path": "packages/registry-vue/components/animated-number/index.ts",
      "content": "export { default as AnimatedNumber } from './AnimatedNumber.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/animated-number/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Tweened number display — counts up on mount and smoothly retargets when the value changes. Ease-out cubic via requestAnimationFrame, custom formatter (Intl-ready), delay/stagger support, honors prefers-reduced-motion, SSR-safe.",
  "categories": [
    "display",
    "data"
  ]
}