{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "toolbar-dock", "type": "registry:ui", "title": "Toolbar Dock", "description": "A Vercel-style floating icon dock with a single tooltip rail that slides and clip-path reveals each label above the hovered button. Spring-driven movement, an options bar that wipes open from a trigger, keyboard shortcut chips, notification dots, and fully configurable items.", "dependencies": [ "@hugeicons/react", "@hugeicons/core-free-icons", "motion" ], "files": [ { "path": "registry/ruixenui/toolbar-dock.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion } from \"motion/react\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n BubbleChatIcon,\n CommandIcon,\n InboxIcon,\n Menu01Icon,\n PencilEdit01Icon,\n Share08Icon,\n ToggleOnIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Toolbar Dock — Vercel-style floating icon dock.\n *\n * A dark pill of icon buttons. One continuous tooltip rail lives above the\n * dock: on hover it slides (x) and clip-paths itself so only the active label\n * is revealed, sitting perfectly above the hovered icon.\n *\n * One item is marked `toggle` and stays pinned on the right. Clicking it\n * collapses the dock to JUST that button and back — the icon strip is an\n * overflow-clipped region whose width springs between 0 and its measured size,\n * so the icons slide out FROM the toggle and tuck back INTO it. The toggle\n * never moves: the wrapper reserves the expanded width as a constant footprint,\n * so the centered pill can't recenter as it resizes.\n *\n * Movement craft:\n * - Tooltip `x` and `clipPath` are spring-driven by Motion in the SAME rAF\n * tick, so the slide and the reveal window never desync.\n * - The collapse width is one spring on a tiny element → cheap, 60fps.\n * - Geometry is read from layout (offsetLeft / offsetWidth), transform-\n * independent, so nothing fights an in-flight animation.\n */\n\nexport interface ToolbarDockItem {\n /** Stable identifier. */\n id: string;\n /** Accessible label, shown in the tooltip rail. */\n label: string;\n /** Icon node — rendered inside the circular button. */\n icon: React.ReactNode;\n /** Keyboard hint chips, e.g. [\"⌘\", \"K\"] or [\"C\"]. */\n shortcut?: string[];\n /** Show a small notification dot on the button. */\n badge?: boolean;\n /** Marks this button as the collapse/expand toggle (pinned, never moves). */\n toggle?: boolean;\n /** Click handler (ignored when `toggle` is set). */\n onClick?: () => void;\n}\n\ninterface ToolbarDockProps {\n items?: ToolbarDockItem[];\n className?: string;\n /** Start collapsed — only the toggle button is visible. */\n defaultCollapsed?: boolean;\n}\n\n/* Springs tuned per role. Clip is near-critically damped (no overshoot); x is a\n touch livelier; the collapse width is smooth with a hint of settle. */\nconst SPRING_X = {\n type: \"spring\" as const,\n stiffness: 650,\n damping: 44,\n mass: 0.7,\n};\nconst SPRING_CLIP = {\n type: \"spring\" as const,\n stiffness: 720,\n damping: 52,\n mass: 0.7,\n};\nconst COLLAPSE_SPRING = {\n type: \"spring\" as const,\n stiffness: 460,\n damping: 42,\n mass: 0.9,\n};\n\nconst ICON_PROPS = { className: \"h-full w-full\", strokeWidth: 2 } as const;\n\nconst DEFAULT_ITEMS: ToolbarDockItem[] = [\n {\n id: \"comment\",\n label: \"Comment\",\n icon: ,\n shortcut: [\"C\"],\n },\n {\n id: \"inbox\",\n label: \"Inbox\",\n icon: ,\n badge: true,\n },\n {\n id: \"flags\",\n label: \"Feature Flags\",\n icon: ,\n },\n {\n id: \"draft\",\n label: \"Draft Mode\",\n icon: ,\n },\n {\n id: \"share\",\n label: \"Share\",\n icon: ,\n },\n {\n id: \"menu\",\n label: \"Menu\",\n icon: ,\n badge: true,\n toggle: true,\n },\n];\n\n/** Sum offsetLeft up the offsetParent chain until `ancestor`. Transform-independent. */\nfunction offsetLeftWithin(\n el: HTMLElement | null,\n ancestor: HTMLElement | null,\n): number {\n let x = 0;\n let node: HTMLElement | null = el;\n while (node && node !== ancestor) {\n x += node.offsetLeft;\n node = node.offsetParent as HTMLElement | null;\n }\n return x;\n}\n\nconst HIDDEN_CLIP = \"inset(0px 100% 0px 0px round 10px)\";\n\nconst useIsoLayoutEffect =\n typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\nexport function ToolbarDock({\n items = DEFAULT_ITEMS,\n className,\n defaultCollapsed = false,\n}: ToolbarDockProps) {\n const wrapperRef = React.useRef(null);\n const railRef = React.useRef(null);\n const stripRef = React.useRef(null);\n const segRefs = React.useRef<(HTMLDivElement | null)[]>([]);\n const btnRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n\n /* Tooltip rail state. */\n const visibleRef = React.useRef(false);\n const appearingRef = React.useRef(true);\n const [visible, setVisible] = React.useState(false);\n const [pos, setPos] = React.useState({ x: 0, clip: HIDDEN_CLIP });\n\n /* Collapse state + measured geometry (strip width + constant footprint). */\n const [collapsed, setCollapsed] = React.useState(defaultCollapsed);\n const [metrics, setMetrics] = React.useState<{\n strip: number;\n footprint: number;\n } | null>(null);\n\n /* Measure the icon strip (always full width — content is absolutely placed)\n and the pill's expanded footprint, before paint. */\n useIsoLayoutEffect(() => {\n const strip = stripRef.current?.offsetWidth ?? 0;\n const footprint = wrapperRef.current?.offsetWidth ?? 0;\n setMetrics({ strip, footprint });\n }, [items]);\n\n const reveal = React.useCallback((index: number) => {\n const rail = railRef.current;\n const seg = segRefs.current[index];\n const btn = btnRefs.current[index];\n const wrapper = wrapperRef.current;\n if (!rail || !seg || !btn || !wrapper) return;\n\n const railWidth = rail.offsetWidth || 1;\n const left = seg.offsetLeft;\n const right = railWidth - seg.offsetLeft - seg.offsetWidth;\n const leftPct = (left / railWidth) * 100;\n const rightPct = (right / railWidth) * 100;\n\n const segCenter = offsetLeftWithin(seg, wrapper) + seg.offsetWidth / 2;\n const btnCenter = offsetLeftWithin(btn, wrapper) + btn.offsetWidth / 2;\n const dx = btnCenter - segCenter;\n\n appearingRef.current = !visibleRef.current;\n visibleRef.current = true;\n\n setVisible(true);\n setPos({\n x: dx,\n clip: `inset(0px ${rightPct}% 0px ${leftPct}% round 10px)`,\n });\n }, []);\n\n const hideTooltip = React.useCallback(() => {\n visibleRef.current = false;\n setVisible(false);\n }, []);\n\n const handleItem = React.useCallback(\n (item: ToolbarDockItem) => {\n if (item.toggle) {\n hideTooltip();\n setCollapsed((c) => !c);\n } else {\n item.onClick?.();\n }\n },\n [hideTooltip],\n );\n\n const appearing = appearingRef.current;\n\n const indexed = items.map((item, index) => ({ item, index }));\n const toggleEntries = indexed.filter((e) => e.item.toggle);\n const iconEntries = indexed.filter((e) => !e.item.toggle);\n\n const renderButton = (item: ToolbarDockItem, index: number) => {\n const isToggle = !!item.toggle;\n return (\n {\n btnRefs.current[index] = el;\n }}\n type=\"button\"\n aria-expanded={isToggle ? !collapsed : undefined}\n aria-label={\n isToggle\n ? collapsed\n ? \"Expand toolbar\"\n : \"Collapse toolbar\"\n : undefined\n }\n tabIndex={!isToggle && collapsed ? -1 : undefined}\n onClick={() => handleItem(item)}\n onMouseEnter={() => reveal(index)}\n onFocus={() => reveal(index)}\n className=\"flex items-center justify-center outline-none\"\n >\n
\n svg]:h-full [&>svg]:w-full\">\n {item.icon}\n \n {item.badge && (\n \n )}\n
\n {item.label}\n \n );\n };\n\n return (\n \n {/* ── Tooltip rail — one surface that slides + clips ── */}\n
\n \n {items.map((item, i) => (\n {\n segRefs.current[i] = el;\n }}\n className=\"z-[1] inline-flex h-8 items-center justify-center\"\n >\n
\n {item.label}\n {item.shortcut && (\n \n {item.shortcut.map((key, k) => (\n \n {key === \"⌘\" ? (\n \n ) : (\n key\n )}\n \n ))}\n \n )}\n
\n
\n ))}\n \n \n\n {/* ── Pill — right-aligned in the constant footprint, so it can shrink\n to the toggle without the toggle ever moving ── */}\n {\n if (!e.currentTarget.contains(e.relatedTarget as Node)) hideTooltip();\n }}\n className=\"relative z-10 flex h-12 items-center rounded-full border border-border bg-background/95 p-2 shadow-lg backdrop-blur\"\n >\n {/* Icon strip — overflow-clipped; width springs 0 ⇄ measured. Content is\n right-anchored so it reveals/retracts from the toggle side. */}\n \n \n {iconEntries.map(({ item, index }) => renderButton(item, index))}\n \n \n\n {/* Pinned toggle — the anchor everything opens/closes from. */}\n {toggleEntries.map(({ item, index }) => (\n
\n {renderButton(item, index)}\n
\n ))}\n \n \n );\n}\n\nexport default ToolbarDock;\n", "type": "registry:ui", "target": "components/ruixen/toolbar-dock.tsx" } ] }