{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "stacked-carousel", "title": "Stacked Carousel", "description": "A self-playing card stack that cycles through items with a smooth, seamless fly-up transition.", "dependencies": [ "motion" ], "files": [ { "path": "registry/new-york/components/stacked-carousel/stacked-carousel.tsx", "type": "registry:component", "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { motion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface StackedCarouselProps {\n /** Any data you want to render. One card per item. */\n items: T[];\n /** Renders the content of a single card. */\n renderItem: (item: T, index: number) => ReactNode;\n /** Milliseconds between automatic advances (default: 1800). */\n interval?: number;\n /** Duration in ms of the fly-up animation before the card cycles (default: 400). */\n advanceDelay?: number;\n /** Height of the stack container (default: 220). */\n containerHeight?: number;\n /** Called whenever the stack advances. */\n onAdvance?: (index: number) => void;\n /** Extra classes for the root wrapper. */\n className?: string;\n /** Extra classes applied to every card. */\n cardClassName?: string;\n}\n\nexport function StackedCarousel({\n items,\n renderItem,\n interval = 1800,\n advanceDelay = 400,\n containerHeight = 220,\n onAdvance,\n className,\n cardClassName = \"\",\n}: StackedCarouselProps) {\n const [activeIndex, setActiveIndex] = useState(0);\n const [isAdvancing, setIsAdvancing] = useState(false);\n const advanceTimer = useRef | null>(null);\n const isAdvancingRef = useRef(false);\n const onAdvanceRef = useRef(onAdvance);\n const activeIndexRef = useRef(activeIndex);\n\n useEffect(() => {\n onAdvanceRef.current = onAdvance;\n }, [onAdvance]);\n\n useEffect(() => {\n activeIndexRef.current = activeIndex;\n }, [activeIndex]);\n\n const advanceStack = useCallback(() => {\n if (isAdvancingRef.current || items.length === 0) return;\n\n isAdvancingRef.current = true;\n setIsAdvancing(true);\n advanceTimer.current = setTimeout(() => {\n const nextIndex = (activeIndexRef.current + 1) % items.length;\n onAdvanceRef.current?.(nextIndex);\n setActiveIndex(nextIndex);\n isAdvancingRef.current = false;\n setIsAdvancing(false);\n }, advanceDelay);\n }, [items.length, advanceDelay]);\n\n useEffect(() => {\n if (items.length < 2) return;\n const timer = window.setInterval(advanceStack, interval);\n return () => window.clearInterval(timer);\n }, [advanceStack, interval, items.length]);\n\n useEffect(() => {\n return () => {\n if (advanceTimer.current) window.clearTimeout(advanceTimer.current);\n };\n }, []);\n\n if (items.length === 0) return null;\n\n const stackHeight = (items.length - 1) * 13 + 82;\n\n return (\n
\n \n
\n {items.map((item, index) => {\n const position = (index - activeIndex + items.length) % items.length;\n const isActive = position === 0;\n\n return (\n \n {renderItem(item, index)}\n \n );\n })}\n
\n
\n \n );\n}\n\nexport default StackedCarousel;\n" } ], "type": "registry:component" }