Bounce Sidebar
nexvyn/bounce-sidebarFreeComponent
A sidebar navigation with a bouncing dot indicator that smoothly animates between items using spring physics.
Live preview
live · rendered by the registry
Rendered by nexvyn on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://ui.nexvyn.dev/r/bounce-sidebar.jsonSource
1'use client'23import { useCallback, useEffect, useRef, useState, type KeyboardEvent } from 'react'4import {5 motion,6 stagger,7 useAnimate,8 useMotionValue,9 useReducedMotion,10 type Transition,11 type Variants,12} from 'motion/react'13import { cn } from '@/lib/utils'14import { playHoverSound, playClickSound, playBounceSound } from '@/lib/sound'1516const EASE_OUT = [0.22, 1, 0.36, 1] as const1718export type BounceSidebarProps = {19 items: string[]20 value?: number21 defaultValue?: number22 onChange?: (index: number) => void23 dotColor?: string24 className?: string25}2627const itemClass = (active: boolean) =>28 cn(29 'flex w-full cursor-pointer items-center rounded-lg p-1 text-left text-base',30 'transition-colors duration-150',31 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 focus-visible:ring-offset-(--color-bg)',32 active ? 'text-foreground' : 'text-foreground/55',33 )3435export function BounceSidebar({36 items,37 value,38 defaultValue = 0,39 onChange,40 dotColor = 'var(--color-accent)',41 className,42}: BounceSidebarProps) {43 const [internalValue, setInternalValue] = useState(defaultValue)44 const activeIndex = value ?? internalValue4546 const [scope, animate] = useAnimate()47 const itemRefs = useRef<(HTMLLIElement | null)[]>([])48 const prevY = useRef<number | null>(null)4950 const reduceMotion = useReducedMotion()5152 const dotX = useMotionValue(0)53 const dotY = useMotionValue(0)5455 const [dotSize, setDotSize] = useState(6)5657 const getTargetY = (index: number, size: number): number | null => {58 const el = itemRefs.current[index]59 if (!el) return null60 const dpr = window.devicePixelRatio || 161 return Math.round((el.offsetTop + el.offsetHeight / 2 - size / 2) * dpr) / dpr62 }6364 useEffect(() => {65 let cancelled = false66 const dpr = window.devicePixelRatio || 167 const computedSize = Math.round(6 * dpr) / dpr6869 const initialIndex = activeIndex70 const snap = () => {71 if (cancelled) return72 const toY = getTargetY(initialIndex, computedSize)73 if (toY === null) return74 dotX.set(0)75 dotY.set(toY)76 prevY.current = toY77 }7879 const ro = new ResizeObserver(snap)80 itemRefs.current.forEach((el) => el && ro.observe(el))81 setDotSize(computedSize)8283 const raf = requestAnimationFrame(snap)84 document.fonts?.ready.then(snap)85 return () => {86 cancelled = true87 cancelAnimationFrame(raf)88 ro.disconnect()89 }90// … truncated
What it pulls in
npm packages
Files it writes
More from nexvyn
All 33 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Badgebadge | nexvyn | Components | Free | 2 deps · 2 files | |
| Bars Themebars-theme | nexvyn | Components | Free | 1 dep · 3 files | |
| Breadcrumbsbreadcrumbs | nexvyn | Components | Free | 1 dep · 2 files | |
| Checkboxcheckbox | nexvyn | Components | Free | 1 dep · 3 files | |
| Clipboard Fieldclipboard-field | nexvyn | Components | Free | no deps · 2 files | |
| Color Pickercolor-picker | nexvyn | Components | Free | no deps · 16 files | |
| Comboboxcombobox | nexvyn | Components | Free | 1 dep · 2 files | |
| Context Menucontext-menu | nexvyn | Components | Free | 1 dep · 2 files |
