{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-number",
  "type": "registry:ui",
  "dependencies": [
    "framer-motion"
  ],
  "files": [
    {
      "path": "components/ui/animated-number.tsx",
      "content": "\"use client\"\n\nimport React, { useEffect, useRef, useState } from 'react'\nimport { motion, AnimatePresence } from \"framer-motion\"\nimport { cn } from \"@/lib/utils\"\n\nfunction AnimatedNumber({ value, className }: { value: number, className?: string }) {\n    return (\n        <div className={cn(\"flex items-center\", className)}>\n            <div className=\"flex relative items-center\">\n                {value.toString().split(\"\").map((digit, index) => (\n                    <SingleNumberHolder key={index} value={digit} index={index} />\n                ))}\n            </div>\n        </div>\n    )\n}\n\nfunction SingleNumberHolder({ value, index }: { value: string, index: number }) {\n    const [height, setHeight] = useState<string | null>(null)\n    const containerRef = useRef<HTMLDivElement>(null)\n    let notANumber = false\n\n    useEffect(() => {\n        if (containerRef.current) {\n            setHeight(getComputedStyle(containerRef.current).height)\n        }\n    }, [])\n\n    if (index === 0) {\n        notANumber = isNaN(Number.parseInt(value))\n    }\n\n    const vars = {\n        init: { opacity: 0 },\n        animate: { opacity: 1 },\n        exit: { opacity: 0 },\n    }\n\n    return (\n        <div\n            className=\"relative\"\n            style={{ height: height || \"auto\", overflowY: \"hidden\", overflowX: \"clip\" }}\n            ref={containerRef}\n        >\n            {notANumber && (\n                <motion.span\n                    initial=\"init\"\n                    animate=\"animate\"\n                    exit=\"exit\"\n                    variants={vars}\n                    key={value}\n                    layout=\"size\"\n                >\n                    {value}\n                </motion.span>\n            )}\n            {!notANumber && <RenderStrip value={value} eleHeight={height} />}\n        </div>\n    )\n}\n\nconst zeroToNine = Array.from({ length: 10 }, (_, k) => k)\n\nfunction RenderStrip({ eleHeight, value }: { eleHeight: string | null, value: string }) {\n    const heightInNumber = Number.parseInt(eleHeight?.replace(\"px\", \"\") || \"48\")\n    const negative = heightInNumber * -1\n    const pos = heightInNumber\n    const prev = useRef(value)\n\n    // Convert string values to numbers for comparison\n    const currentVal = parseInt(value)\n    const prevVal = parseInt(prev.current)\n\n    // Calculate direction based on value change\n    const diff = prevVal - currentVal\n    const dir = currentVal > prevVal ? pos * diff * -1 : negative * diff\n\n    // Update ref after calculation\n    useEffect(() => {\n        prev.current = value\n    }, [value])\n\n    return (\n        <AnimatePresence mode='wait'>\n            <motion.div\n                key={value}\n                initial={{ y: dir }}\n                animate={{ y: 0 }}\n                exit={{ y: 0, transition: { duration: 0.1 } }}\n                transition={{ duration: 0.5, ease: \"easeOut\" }}\n                className='flex relative flex-col'\n            >\n                {/* Numbers smaller than current */}\n                <motion.span\n                    layout\n                    key={`negative-${value}`}\n                    className={cn('flex flex-col items-center absolute bottom-full left-0')}\n                >\n                    {zeroToNine.filter(val => val < currentVal).map((val, idx) => (\n                        <span key={`${val}_${idx}`}>{val}</span>\n                    ))}\n                </motion.span>\n\n                {/* Current Number */}\n                <span key={`current-${value}`}>{value}</span>\n\n                {/* Numbers larger than current */}\n                <motion.span\n                    layout\n                    key={`positive-${value}`}\n                    className={cn('flex flex-col items-center absolute top-full left-0')}\n                >\n                    {zeroToNine.filter(val => val > currentVal).map((val, idx) => (\n                        <span key={`${val}_${idx}`}>{val}</span>\n                    ))}\n                </motion.span>\n            </motion.div>\n        </AnimatePresence>\n    )\n}\n\n// Score-style animated number with color feedback\nfunction AnimatedScore({ value, duration = 0.2, className }: { value: number, duration?: number, className?: string }) {\n    const prevValueRef = useRef(value)\n\n    useEffect(() => {\n        prevValueRef.current = value\n    }, [value])\n\n    const colors = {\n        negative: \"#37ff1a\",\n        positive: \"#ff1a4b\",\n        neutral: \"#fff\"\n    }\n\n    const transforVal = 80\n    const forwards = {\n        init: { y: transforVal * -1, opacity: 0, scale: 0.5, color: colors.negative },\n        animate: {\n            y: 0,\n            opacity: 1,\n            scale: [1.7, 1],\n            color: [colors.negative, colors.negative, colors.neutral],\n            transition: { duration: 0.4, times: [0, 0.7, 1], color: { times: [0, 0.75, 0.9] } },\n        },\n        exit: {\n            y: transforVal,\n            opacity: 0,\n            scale: 0.5,\n            color: colors.positive\n        },\n    }\n\n    const backwards = {\n        init: { y: transforVal, opacity: 0, scale: 0.5, color: colors.positive },\n        animate: {\n            y: 0,\n            opacity: 1,\n            scale: [1.7, 1],\n            color: [colors.positive, colors.positive, colors.neutral],\n            transition: { duration: 0.4, times: [0, 0.7, 1], color: { times: [0, 0.75, 0.9] } },\n        },\n        exit: {\n            y: transforVal * -1,\n            opacity: 0,\n            scale: 0.5,\n            color: colors.negative\n        }\n    }\n\n    const variants = value >= prevValueRef.current ? forwards : backwards\n    const direction = value >= prevValueRef.current ? \"forwards\" : \"backwards\"\n\n    return (\n        <div className={cn(\"relative flex justify-center items-center py-1 px-2 w-full rounded-md\", className)}>\n            <motion.div layout=\"size\" className='w-fit flex justify-center items-center'>\n                {value.toString().split(\"\").map((number, index) => (\n                    <ScoreContainer\n                        direction={direction}\n                        duration={duration}\n                        variants={variants}\n                        number={number}\n                        key={index}\n                    />\n                ))}\n            </motion.div>\n        </div>\n    )\n}\n\nfunction ScoreContainer({ number, variants, duration = 0.7, direction }: {\n    number: string,\n    variants: any,\n    duration?: number,\n    direction: string\n}) {\n    const cached = React.useMemo(() => (\n        <div className='relative'>\n            <AnimatePresence mode='popLayout'>\n                <motion.div\n                    animate=\"animate\"\n                    className='flex justify-center items-center'\n                    initial=\"init\"\n                    exit=\"exit\"\n                    variants={variants}\n                    key={number.toString()}\n                    layout=\"size\"\n                    transition={{ duration, ease: \"backInOut\" }}\n                >\n                    {number}\n                </motion.div>\n            </AnimatePresence>\n        </div>\n    ), [number, direction, variants, duration])\n\n    return <React.Fragment>{cached}</React.Fragment>\n}\n\nexport { AnimatedNumber, AnimatedScore }\n",
      "type": "registry:ui",
      "target": "components/ui/animated-number.tsx"
    }
  ]
}