{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "animated-list", "title": "Animated List", "description": "A generic animated list with keyboard navigation and auto-scroll.", "dependencies": [ "motion" ], "files": [ { "path": "registry/new-york/components/animated-list/animated-list.tsx", "type": "registry:component", "content": "\"use client\";\n\nimport React, {\n useRef,\n useState,\n useEffect,\n useCallback,\n ReactNode,\n} from \"react\";\nimport { motion, useScroll, useTransform } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface AnimatedListProps {\n items: T[];\n renderItem: (\n item: T,\n index: number,\n isSelected: boolean,\n onMouseEnter: () => void,\n onClick: () => void,\n listRef: React.RefObject,\n ) => ReactNode;\n className?: string;\n}\n\nexport function AnimatedList({\n items,\n renderItem,\n className,\n}: AnimatedListProps) {\n const listRef = useRef(null);\n const [selectedIndex, setSelectedIndex] = useState(-1);\n const keyboardNavRef = useRef(false);\n\n // Performance Optimization: GPU Accelerated Scroll Gradients\n const { scrollYProgress } = useScroll({ container: listRef });\n const topGradientOpacity = useTransform(scrollYProgress, [0, 0.05], [0, 1]);\n const bottomGradientOpacity = useTransform(\n scrollYProgress,\n [0.95, 1],\n [1, 0],\n );\n\n const handleItemMouseEnter = useCallback((index: number) => {\n setSelectedIndex(index);\n }, []);\n\n const handleItemClick = useCallback((index: number) => {\n setSelectedIndex(index);\n }, []);\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowDown\" || (e.key === \"Tab\" && !e.shiftKey)) {\n e.preventDefault();\n keyboardNavRef.current = true;\n setSelectedIndex((prev) => Math.min(prev + 1, items.length - 1));\n } else if (e.key === \"ArrowUp\" || (e.key === \"Tab\" && e.shiftKey)) {\n e.preventDefault();\n keyboardNavRef.current = true;\n setSelectedIndex((prev) => Math.max(prev - 1, 0));\n } else if (e.key === \"Enter\") {\n if (selectedIndex >= 0 && selectedIndex < items.length) {\n e.preventDefault();\n handleItemClick(selectedIndex);\n }\n }\n };\n\n useEffect(() => {\n if (!keyboardNavRef.current || selectedIndex < 0 || !listRef.current) return;\n keyboardNavRef.current = false;\n const container = listRef.current;\n const selectedItem = container.querySelector(\n `[data-index=\"${selectedIndex}\"]`,\n ) as HTMLElement | null;\n\n if (selectedItem) {\n const extraMargin = 50;\n const containerScrollTop = container.scrollTop;\n const containerHeight = container.clientHeight;\n const itemTop = selectedItem.offsetTop;\n const itemBottom = itemTop + selectedItem.offsetHeight;\n if (itemTop < containerScrollTop + extraMargin) {\n container.scrollTo({ top: itemTop - extraMargin, behavior: \"smooth\" });\n } else if (\n itemBottom >\n containerScrollTop + containerHeight - extraMargin\n ) {\n container.scrollTo({\n top: itemBottom - containerHeight + extraMargin,\n behavior: \"smooth\",\n });\n }\n }\n }, [selectedIndex]);\n\n return (\n
\n \n {items.map((item, index) => (\n \n {renderItem(\n item,\n index,\n selectedIndex === index,\n () => handleItemMouseEnter(index),\n () => handleItemClick(index),\n listRef,\n )}\n \n ))}\n \n\n {/* Top Gradient */}\n \n\n {/* Bottom Gradient */}\n \n
\n );\n}\n" } ], "type": "registry:component" }