{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "statscarousel",
  "type": "registry:ui",
  "title": "Stats Carousel",
  "description": "Animated statistics counter in stacked carousel for modern websites.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["motion"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/statscarousel.tsx",
      "content": "'use client';\n\nimport { useEffect, useState, useRef } from 'react';\nimport {\n  motion,\n  useMotionValue,\n  useSpring,\n  useTransform,\n  AnimatePresence,\n} from 'motion/react';\n\ninterface StatItem {\n  id?: number;\n  value: number;\n  suffix?: string;\n  label: string;\n}\n\nfunction StatsCarousel({\n  value,\n  suffix,\n  trigger,\n  onDone,\n}: {\n  value: number;\n  suffix?: string;\n  trigger: number;\n  onDone?: () => void;\n}) {\n  const motionValue = useMotionValue(0);\n  const springValue = useSpring(motionValue, {\n    damping: 20,\n    stiffness: 50,\n    mass: 1,\n  });\n\n  const rounded = useTransform(springValue, (latest) =>\n    Number(latest.toFixed(value % 1 === 0 ? 0 : 1)),\n  );\n\n  const [displayValue, setDisplayValue] = useState(0);\n\n  useEffect(() => {\n    motionValue.set(0);\n    let animationComplete = false;\n\n    const unsub = rounded.on('change', (v) => {\n      setDisplayValue(v);\n      if (v >= value && !animationComplete) {\n        animationComplete = true;\n        onDone?.();\n      }\n    });\n\n    const timeout = setTimeout(() => {\n      motionValue.set(value);\n    }, 100);\n\n    return () => {\n      unsub();\n      clearTimeout(timeout);\n    };\n  }, [trigger, value, motionValue, rounded, onDone]);\n\n  return (\n    <div className='text-5xl font-extrabold text-white dark:text-black'>\n      {displayValue}\n      {suffix}\n    </div>\n  );\n}\n\nexport default function StatsCarouselCount({\n  stats,\n  title,\n  className = '',\n  cardClassName = '',\n  animation,\n}: {\n  stats?: StatItem[];\n  title?: string;\n  className?: string;\n  cardClassName?: string;\n  animation?: 'drag';\n}) {\n  const defaultStats: StatItem[] = [\n    { value: 50, suffix: '+', label: 'Components' },\n    { value: 12, suffix: 'K+', label: 'Developers' },\n    { value: 99, suffix: '%', label: 'Performance' },\n  ];\n\n  const initialStats = (stats ?? defaultStats).map((s, i) => ({\n    ...s,\n    id: i + 1,\n  }));\n\n  const [items, setItems] = useState(initialStats);\n  const [triggerCounter, setTriggerCounter] = useState(1);\n\n  const [phase, setPhase] = useState<'idle' | 'down' | 'stackUp' | 'upReenter'>(\n    'idle',\n  );\n  const [activeTopId, setActiveTopId] = useState(initialStats[0].id!);\n  const [animatedIds, setAnimatedIds] = useState<Set<number>>(new Set());\n  const [resetQueue, setResetQueue] = useState<Set<number>>(new Set());\n\n  const timeoutRef = useRef<NodeJS.Timeout | null>(null);\n  const autoPlayTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n  const isDragMode = animation === 'drag';\n\n  useEffect(() => {\n    if (isDragMode) return;\n\n    const startAutoPlay = () => {\n      autoPlayTimeoutRef.current = setTimeout(() => {\n        if (phase === 'idle') {\n          setPhase('down');\n        }\n      }, 3000);\n    };\n\n    if (phase === 'idle') {\n      startAutoPlay();\n    }\n\n    return () => {\n      if (autoPlayTimeoutRef.current) {\n        clearTimeout(autoPlayTimeoutRef.current);\n      }\n    };\n  }, [isDragMode, phase]);\n\n  useEffect(() => {\n    if (isDragMode) return;\n\n    if (phase === 'down') {\n      timeoutRef.current = setTimeout(() => setPhase('stackUp'), 600);\n    } else if (phase === 'stackUp') {\n      timeoutRef.current = setTimeout(() => setPhase('upReenter'), 600);\n    } else if (phase === 'upReenter') {\n      timeoutRef.current = setTimeout(() => {\n        setItems((prev) => {\n          const [first, ...rest] = prev;\n          const newTopId = rest[0].id!;\n\n          setResetQueue((r) => {\n            const newSet = new Set(r);\n            newSet.add(first.id!);\n            return newSet;\n          });\n\n          setAnimatedIds((prev) => {\n            const copy = new Set(prev);\n            copy.delete(first.id!);\n            return copy;\n          });\n\n          setActiveTopId(newTopId);\n          setTriggerCounter((t) => t + 1);\n\n          return [...rest, first];\n        });\n\n        setPhase('idle');\n      }, 600);\n    }\n\n    return () => {\n      if (timeoutRef.current) {\n        clearTimeout(timeoutRef.current);\n      }\n    };\n  }, [isDragMode, phase]);\n\n  const handleDragEnd = (\n    _: MouseEvent | TouchEvent | PointerEvent,\n    info: { offset: { y: number } },\n  ) => {\n    if (!isDragMode) return;\n\n    const dragDistance = info.offset.y;\n\n    if (dragDistance > 100) {\n      setItems((prev) => {\n        const [first, ...rest] = prev;\n        setTriggerCounter((t) => t + 1);\n        return [...rest, first];\n      });\n    }\n  };\n\n  return (\n    <section\n      className={`py-20 px-4 w-full max-w-md mx-auto text-center relative h-125 z-40 ${className}`}\n    >\n      <h2 className='text-lg font-bold text-black dark:text-white mb-12'>\n        {title ?? 'CREATE STUNNING INTERFACES WITH SCROLLX UI COMPONENTS'}\n      </h2>\n\n      <div className='relative h-75'>\n        <AnimatePresence>\n          {items.map((stat, index) => {\n            const baseY = index * 20;\n            const scale = 1 - index * 0.05;\n            const isTopCard = index === 0;\n            const bottomIndex = items.length - 1;\n            const bottomScale = 1 - bottomIndex * 0.05;\n\n            let animate = { x: 0, y: baseY, scale };\n\n            if (!isDragMode) {\n              if (isTopCard && phase === 'down') {\n                animate = { x: 0, y: baseY + 150, scale: 0.8 };\n              }\n              if (!isTopCard && phase === 'stackUp') {\n                animate = { x: 0, y: baseY - 20, scale };\n              }\n              if (isTopCard && phase === 'stackUp') {\n                animate = { x: 0, y: baseY + 150, scale: 0.8 };\n              }\n              if (isTopCard && phase === 'upReenter') {\n                animate = { x: 0, y: bottomIndex * 20, scale: bottomScale };\n              }\n            }\n\n            const zIndex =\n              !isDragMode && phase === 'upReenter' && isTopCard\n                ? 0\n                : Math.max(0, Math.min(40, 40 - index));\n\n            const shouldAutoAnimate =\n              !isDragMode &&\n              stat.id === activeTopId &&\n              phase === 'idle' &&\n              !animatedIds.has(stat.id!);\n\n            const shouldDragAnimate = isDragMode && isTopCard;\n\n            const shouldShowZero =\n              !isDragMode &&\n              resetQueue.has(stat.id!) &&\n              stat.id !== activeTopId;\n\n            return (\n              <motion.div\n                key={stat.id}\n                className='absolute left-0 right-0 mx-auto w-full touch-none'\n                style={{ zIndex }}\n                animate={animate}\n                transition={{ duration: 0.6, ease: 'easeInOut' }}\n                drag={isDragMode && isTopCard ? 'y' : false}\n                dragConstraints={{ top: 0, bottom: 150 }}\n                dragElastic={0.3}\n                onDragEnd={handleDragEnd}\n                whileHover={\n                  isDragMode && isTopCard ? { scale: 1.02, cursor: 'grab' } : {}\n                }\n                whileDrag={\n                  isDragMode ? { scale: 0.95, cursor: 'grabbing' } : {}\n                }\n              >\n                <div\n                  className={`flex flex-col items-center justify-center rounded-xl border border-neutral-800 p-6 bg-neutral-950 dark:border-neutral-200 dark:bg-neutral-50 shadow-lg ${\n                    isDragMode && isTopCard\n                      ? 'hover:shadow-xl transition-shadow duration-200'\n                      : ''\n                  } ${cardClassName}`}\n                >\n                  {shouldAutoAnimate ? (\n                    <StatsCarousel\n                      value={stat.value}\n                      suffix={stat.suffix}\n                      trigger={triggerCounter}\n                      onDone={() =>\n                        setAnimatedIds((prev) => new Set(prev).add(stat.id!))\n                      }\n                    />\n                  ) : shouldDragAnimate ? (\n                    <StatsCarousel\n                      value={stat.value}\n                      suffix={stat.suffix}\n                      trigger={triggerCounter}\n                    />\n                  ) : shouldShowZero ? (\n                    <div className='text-5xl font-extrabold text-white dark:text-black'>\n                      0{stat.suffix}\n                    </div>\n                  ) : (\n                    <div className='text-5xl font-extrabold text-white dark:text-black'>\n                      {stat.value}\n                      {stat.suffix}\n                    </div>\n                  )}\n                  <p className='text-xs text-neutral-400 dark:text-neutral-800 mt-2 text-center uppercase tracking-wide'>\n                    {stat.label}\n                  </p>\n                </div>\n              </motion.div>\n            );\n          })}\n        </AnimatePresence>\n      </div>\n    </section>\n  );\n}\n"
    }
  ]
}
