{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "widget-stack", "title": "Widget Stack", "description": "A stack of small or medium widgets with vertical snap scrolling and page dots.", "dependencies": [], "registryDependencies": [], "files": [ { "path": "registry/spark-ui/widget-stack.tsx", "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\nexport type WidgetStackSize = \"small\" | \"medium\";\n\nexport interface WidgetStackProps {\n children: React.ReactNode;\n size?: WidgetStackSize;\n defaultIndex?: number;\n onIndexChange?: (index: number) => void;\n className?: string;\n ariaLabel?: string;\n}\n\nexport function WidgetStack({\n children,\n size = \"small\",\n defaultIndex = 0,\n onIndexChange,\n className,\n ariaLabel = \"Widget stack\",\n}: WidgetStackProps) {\n const pages = React.Children.toArray(children);\n const initialIndex = Math.min(Math.max(defaultIndex, 0), pages.length - 1);\n const [activeIndex, setActiveIndex] = React.useState(initialIndex);\n const [scrollProgress, setScrollProgress] = React.useState(initialIndex);\n const scrollRef = React.useRef(null);\n\n const scrollToIndex = React.useCallback(\n (index: number, behavior: ScrollBehavior = \"smooth\") => {\n const container = scrollRef.current;\n if (!container) return;\n\n const nextIndex = Math.min(Math.max(index, 0), pages.length - 1);\n container.scrollTo({\n top: nextIndex * container.clientHeight,\n behavior:\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n ? \"auto\"\n : behavior,\n });\n },\n [pages.length],\n );\n\n React.useEffect(() => {\n scrollToIndex(initialIndex, \"auto\");\n }, [initialIndex, scrollToIndex]);\n\n if (pages.length === 0) return null;\n\n return (\n \n {\n const container = event.currentTarget;\n const progress = Math.min(\n container.scrollTop / container.clientHeight,\n pages.length - 1,\n );\n const index = Math.round(progress);\n setScrollProgress(progress);\n if (index !== activeIndex) {\n setActiveIndex(index);\n onIndexChange?.(index);\n }\n }}\n onKeyDown={(event) => {\n if (event.target !== event.currentTarget) return;\n\n const nextIndex =\n event.key === \"ArrowDown\" || event.key === \"PageDown\"\n ? activeIndex + 1\n : event.key === \"ArrowUp\" || event.key === \"PageUp\"\n ? activeIndex - 1\n : event.key === \"Home\"\n ? 0\n : event.key === \"End\"\n ? pages.length - 1\n : null;\n\n if (nextIndex !== null) {\n event.preventDefault();\n scrollToIndex(nextIndex);\n }\n }}\n className=\"flex h-full w-full snap-y snap-mandatory flex-col overflow-y-auto overscroll-contain rounded-[1.35rem] bg-card shadow-[0_12px_35px_rgba(0,0,0,0.2)] outline-none ring-1 ring-black/5 scroll-smooth focus-visible:ring-2 focus-visible:ring-foreground/40 motion-reduce:scroll-auto dark:ring-white/10 [&::-webkit-scrollbar]:hidden [scrollbar-width:none]\"\n >\n {pages.map((page, index) => (\n \n {page}\n \n ))}\n \n\n {pages.length > 1 && (\n \n \n {pages.map((_, index) => (\n scrollToIndex(index)}\n className=\"grid size-5 place-items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n \n \n ))}\n \n )}\n \n );\n}\n", "type": "registry:component" } ], "type": "registry:component" }