{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-list",
  "title": "Animated List",
  "description": "Looping feed that springs each item in, newest on top.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/animated-list.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useMemo, useState, useSyncExternalStore } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst noopSubscribe = () => () => {};\n\ninterface AnimatedListProps {\n  children: React.ReactNode;\n  className?: string;\n  /** ms between items appearing */\n  delay?: number;\n}\n\n/**\n * Reveals children one by one, newest on top, then loops.\n * Wrap in a fixed-height container with a bottom mask for a feed effect.\n * The loop pauses while hovered or focused; under reduced motion all items\n * are shown at once.\n */\nexport function AnimatedList({\n  children,\n  className,\n  delay = 2000,\n}: AnimatedListProps) {\n  const [index, setIndex] = useState(0);\n  const [hovered, setHovered] = useState(false);\n  const [focused, setFocused] = useState(false);\n  // useReducedMotion() already knows the preference on the first client\n  // render, but the server can't — keep the animated markup until hydrated.\n  const hydrated = useSyncExternalStore(noopSubscribe, () => true, () => false);\n  const reducedMotion = useReducedMotion() === true && hydrated;\n  const items = useMemo(() => React.Children.toArray(children), [children]);\n  const paused = hovered || focused;\n\n  useEffect(() => {\n    if (reducedMotion || paused) return;\n    const interval = setInterval(\n      () => setIndex((i) => (i + 1) % items.length),\n      delay\n    );\n    return () => clearInterval(interval);\n  }, [items.length, delay, reducedMotion, paused]);\n\n  const visible = useMemo(\n    () => (reducedMotion ? items : items.slice(0, index + 1)).slice().reverse(),\n    [items, index, reducedMotion]\n  );\n\n  return (\n    <div\n      data-slot=\"animated-list\"\n      className={cn(\"flex flex-col gap-3\", className)}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      onFocus={() => setFocused(true)}\n      onBlur={(e) => {\n        if (!e.currentTarget.contains(e.relatedTarget)) setFocused(false);\n      }}\n    >\n      <AnimatePresence initial={false}>\n        {visible.map((item) => (\n          <motion.div\n            key={(item as React.ReactElement).key}\n            layout={!reducedMotion}\n            initial={reducedMotion ? false : { scale: 0, opacity: 0 }}\n            animate={{ scale: 1, opacity: 1, originY: 0 }}\n            exit={reducedMotion ? undefined : { scale: 0, opacity: 0 }}\n            transition={\n              reducedMotion\n                ? { duration: 0 }\n                : { type: \"spring\", stiffness: 350, damping: 40 }\n            }\n            className=\"w-full\"\n          >\n            {item}\n          </motion.div>\n        ))}\n      </AnimatePresence>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/animated-list.tsx"
    }
  ],
  "type": "registry:ui"
}