{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "bounce-sidebar", "title": "Bounce Sidebar", "description": "A vertical navigation list with a bouncy, spring-animated active indicator.", "dependencies": [ "motion" ], "registryDependencies": [ "amitgajare2/ariseui/utils" ], "files": [ { "path": "components/ui/bounce-sidebar.tsx", "content": "\"use client\";\r\n\r\nimport { useEffect, useLayoutEffect, useRef, useState, type ComponentProps } from \"react\";\r\nimport Link from \"next/link\";\r\nimport { motion, useAnimate } from \"motion/react\";\r\nimport { arc } from \"motion\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nconst MotionLink = motion.create(Link);\r\n\r\nconst useIsomorphicLayoutEffect =\r\n typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\r\n\r\nexport type BounceSidebarItem = string | { label: string; href?: string };\r\n\r\nexport type BounceSidebarProps = Omit, \"onChange\"> & {\r\n items: BounceSidebarItem[];\r\n value?: number;\r\n defaultValue?: number;\r\n onChange?: (index: number) => void;\r\n dotColor?: string;\r\n};\r\n\r\nexport function BounceSidebar({\r\n items,\r\n value,\r\n defaultValue = 0,\r\n onChange,\r\n dotColor = \"#fcd601\",\r\n className,\r\n ...props\r\n}: BounceSidebarProps) {\r\n const [internalValue, setInternalValue] = useState(defaultValue);\r\n const activeIndex = value ?? internalValue;\r\n\r\n const [dot, animate] = useAnimate();\r\n const itemRefs = useRef<(HTMLLIElement | null)[]>([]);\r\n const prevY = useRef(null);\r\n\r\n const [dotSize, setDotSize] = useState(6);\r\n const [ready, setReady] = useState(false);\r\n useEffect(() => {\r\n const dpr = window.devicePixelRatio || 1;\r\n setDotSize(Math.round(6 * dpr) / dpr);\r\n }, []);\r\n\r\n useIsomorphicLayoutEffect(() => {\r\n let cancelled = false;\r\n const snap = () => {\r\n const el = itemRefs.current[activeIndex];\r\n if (cancelled || !el || !dot.current) return;\r\n const dpr = window.devicePixelRatio || 1;\r\n const size = Math.round(6 * dpr) / dpr;\r\n const toY =\r\n Math.round((el.offsetTop + el.offsetHeight / 2 - size / 2) * dpr) / dpr;\r\n animate(dot.current, { x: 0, y: toY }, { duration: 0 });\r\n prevY.current = toY;\r\n setReady(true);\r\n };\r\n\r\n snap();\r\n const raf = requestAnimationFrame(snap);\r\n document.fonts?.ready.then(snap);\r\n return () => {\r\n cancelled = true;\r\n cancelAnimationFrame(raf);\r\n };\r\n }, []);\r\n\r\n useEffect(() => {\r\n const el = itemRefs.current[activeIndex];\r\n if (!el || !dot.current) return;\r\n\r\n const dpr = window.devicePixelRatio || 1;\r\n const toY =\r\n Math.round((el.offsetTop + el.offsetHeight / 2 - dotSize / 2) * dpr) /\r\n dpr;\r\n\r\n if (prevY.current === null) {\r\n animate(dot.current, { x: 0, y: toY }, { duration: 0 });\r\n prevY.current = toY;\r\n return;\r\n }\r\n\r\n const fromY = prevY.current;\r\n const delta = toY - fromY;\r\n prevY.current = toY;\r\n if (delta === 0) return;\r\n\r\n const distance = Math.abs(delta);\r\n const path = arc({\r\n strength: Math.min(0.8, 14 / distance),\r\n direction: delta > 0 ? \"ccw\" : \"cw\",\r\n });\r\n\r\n animate(\r\n dot.current,\r\n { x: 0, y: toY },\r\n { duration: 0.25, ease: \"easeOut\", path },\r\n );\r\n }, [activeIndex, animate, dot, dotSize]);\r\n\r\n const select = (index: number) => {\r\n if (value === undefined) setInternalValue(index);\r\n onChange?.(index);\r\n };\r\n\r\n return (\r\n \r\n \r\n\r\n {items.map((item, index) => {\r\n const label = typeof item === \"string\" ? item : item.label;\r\n const href = typeof item === \"string\" ? undefined : item.href;\r\n const isActive = index === activeIndex;\r\n const itemClassName = cn(\r\n \"flex w-full cursor-pointer items-center rounded-lg p-1 text-left text-sm transition-colors duration-200\",\r\n isActive ? \"text-foreground\" : \"text-foreground/50\",\r\n );\r\n\r\n return (\r\n {\r\n itemRefs.current[index] = el;\r\n }}\r\n >\r\n {href ? (\r\n select(index)}\r\n className={itemClassName}\r\n >\r\n {label}\r\n \r\n ) : (\r\n select(index)}\r\n className={itemClassName}\r\n >\r\n {label}\r\n \r\n )}\r\n \r\n );\r\n })}\r\n \r\n );\r\n}\r\n", "type": "registry:ui" } ], "type": "registry:ui" }