{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animate-digits",
  "title": "Animate Digits",
  "description": "Per-digit blur-slide animation — only the digits that change animate.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/components/unlumen/animate-digits/index.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { AnimatePresence, motion, useSpring, useTransform } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface AnimateDigitsProps {\n  value: string;\n  gap?: number;\n  className?: string;\n  digitClassName?: string;\n  enterStiffness?: number;\n  enterDamping?: number;\n  exitStiffness?: number;\n  exitDamping?: number;\n  direction?: \"dynamic\" | \"up\" | \"down\";\n  enterY?: number;\n  enterBlur?: number;\n  enterScale?: number;\n  animationDelay?: number;\n}\n\ninterface ExitItem {\n  id: number;\n  char: string;\n  exitY: number;\n}\n\nlet _id = 0;\n\nfunction DigitCell({\n  char,\n  isDigit,\n  className,\n  enterStiffness = 170,\n  enterDamping = 10,\n  exitStiffness = 170,\n  exitDamping = 15,\n  direction = \"dynamic\",\n  enterY = 32,\n  enterBlur = 52,\n  enterScale = 0.7,\n}: {\n  char: string;\n  isDigit: boolean;\n  className?: string;\n  enterStiffness?: number;\n  enterDamping?: number;\n  exitStiffness?: number;\n  exitDamping?: number;\n  direction?: \"dynamic\" | \"up\" | \"down\";\n  enterY?: number;\n  enterBlur?: number;\n  enterScale?: number;\n}) {\n  const [exitQueue, setExitQueue] = useState<ExitItem[]>([]);\n\n  const prevCharRef = useRef(char);\n  const isFirstRender = useRef(true);\n\n  // Springs — jump() snaps the spring itself (position + velocity reset)\n  const springConfig = { stiffness: enterStiffness, damping: enterDamping };\n  const y = useSpring(0, springConfig);\n  const opacity = useSpring(1, springConfig);\n  const scale = useSpring(1, springConfig);\n  const blur = useSpring(0, springConfig);\n  const filter = useTransform(blur, (v) => `blur(${v}px)`);\n\n  useEffect(() => {\n    if (!isDigit) return;\n\n    const prev = prevCharRef.current;\n    prevCharRef.current = char;\n\n    if (isFirstRender.current) {\n      isFirstRender.current = false;\n      return;\n    }\n\n    if (char === prev || !/\\d/.test(prev)) return;\n\n    const up =\n      direction === \"dynamic\"\n        ? Number(char) > Number(prev)\n        : direction === \"up\";\n\n    const id = _id++;\n    setExitQueue((q) => {\n      const next = [...q, { id, char: prev, exitY: up ? -enterY : enterY }];\n      return next.length > 3 ? next.slice(-3) : next;\n    });\n\n    y.jump(up ? enterY : -enterY);\n    opacity.jump(0);\n    scale.jump(enterScale);\n    blur.jump(enterBlur);\n\n    y.set(0);\n    opacity.set(1);\n    scale.set(1);\n    blur.set(0);\n  }, [\n    char,\n    isDigit,\n    direction,\n    enterY,\n    enterBlur,\n    enterScale,\n    y,\n    opacity,\n    scale,\n    blur,\n  ]);\n\n  if (!isDigit) {\n    return <span className={className}>{char}</span>;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"relative grid place-items-center [&>*]:col-start-1 [&>*]:row-start-1\",\n        className,\n      )}\n    >\n      <AnimatePresence>\n        {exitQueue.map(({ id, char: exitChar, exitY }) => (\n          <motion.span\n            key={id}\n            aria-hidden\n            initial={{ opacity: 1, scale: 1, filter: \"blur(0px)\", y: 0 }}\n            animate={{ opacity: 0, scale: 0.7, filter: \"blur(10px)\", y: exitY }}\n            transition={{\n              type: \"spring\",\n              stiffness: exitStiffness,\n              damping: exitDamping,\n            }}\n            onAnimationComplete={() =>\n              setExitQueue((q) => q.filter((item) => item.id !== id))\n            }\n          >\n            {exitChar}\n          </motion.span>\n        ))}\n      </AnimatePresence>\n      <motion.span style={{ opacity, scale, filter, y }}>{char}</motion.span>\n    </div>\n  );\n}\n\nfunction AnimateDigits({\n  value,\n  gap = 2,\n  className,\n  digitClassName,\n  enterStiffness,\n  enterDamping,\n  exitStiffness,\n  exitDamping,\n  direction,\n  enterY,\n  enterBlur,\n  enterScale,\n  animationDelay = 80,\n}: AnimateDigitsProps) {\n  // displayedValue is what the cells actually render — advanced one step at a time\n  const [displayedValue, setDisplayedValue] = useState(value);\n\n  const pendingQueue = useRef<string[]>([]);\n  const isAnimating = useRef(false);\n  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const displayedRef = useRef(value);\n\n  const processQueue = () => {\n    if (pendingQueue.current.length === 0) {\n      isAnimating.current = false;\n      return;\n    }\n\n    isAnimating.current = true;\n    const next = pendingQueue.current.shift()!;\n    displayedRef.current = next;\n    setDisplayedValue(next);\n\n    timerRef.current = setTimeout(processQueue, animationDelay);\n  };\n\n  useEffect(() => {\n    if (value === displayedRef.current) return;\n\n    pendingQueue.current.push(value);\n\n    if (!isAnimating.current) {\n      processQueue();\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [value]);\n\n  // When animationDelay changes while animating, the in-flight timer keeps the\n  // old delay for its last tick — acceptable; no need to restart the queue.\n\n  useEffect(() => {\n    return () => {\n      if (timerRef.current) clearTimeout(timerRef.current);\n    };\n  }, []);\n\n  const chars = displayedValue.split(\"\");\n\n  return (\n    <div\n      className={cn(\"flex items-center tabular-nums\", className)}\n      style={{ gap }}\n    >\n      {chars.map((char, i) => (\n        <DigitCell\n          key={i}\n          char={char}\n          isDigit={/\\d/.test(char)}\n          className={digitClassName}\n          enterStiffness={enterStiffness}\n          enterDamping={enterDamping}\n          exitStiffness={exitStiffness}\n          exitDamping={exitDamping}\n          direction={direction}\n          enterY={enterY}\n          enterBlur={enterBlur}\n          enterScale={enterScale}\n        />\n      ))}\n    </div>\n  );\n}\n\nexport { AnimateDigits, type AnimateDigitsProps };\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/animate-digits.tsx"
    }
  ],
  "type": "registry:ui"
}