{
  "$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: <HugeiconsIcon icon={BubbleChatIcon} {...ICON_PROPS} />,\n    shortcut: [\"C\"],\n  },\n  {\n    id: \"inbox\",\n    label: \"Inbox\",\n    icon: <HugeiconsIcon icon={InboxIcon} {...ICON_PROPS} />,\n    badge: true,\n  },\n  {\n    id: \"flags\",\n    label: \"Feature Flags\",\n    icon: <HugeiconsIcon icon={ToggleOnIcon} {...ICON_PROPS} />,\n  },\n  {\n    id: \"draft\",\n    label: \"Draft Mode\",\n    icon: <HugeiconsIcon icon={PencilEdit01Icon} {...ICON_PROPS} />,\n  },\n  {\n    id: \"share\",\n    label: \"Share\",\n    icon: <HugeiconsIcon icon={Share08Icon} {...ICON_PROPS} />,\n  },\n  {\n    id: \"menu\",\n    label: \"Menu\",\n    icon: <HugeiconsIcon icon={Menu01Icon} {...ICON_PROPS} />,\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<HTMLDivElement>(null);\n  const railRef = React.useRef<HTMLDivElement>(null);\n  const stripRef = React.useRef<HTMLDivElement>(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      <button\n        key={item.id}\n        ref={(el) => {\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        <div className=\"group relative flex size-8 items-center justify-center rounded-full p-1.5 transition-colors hover:bg-accent\">\n          <span className=\"h-full w-full [&>svg]:h-full [&>svg]:w-full\">\n            {item.icon}\n          </span>\n          {item.badge && (\n            <span className=\"absolute right-1.5 top-1.5 size-2 rounded-full border-[1.7px] border-background bg-primary transition-colors group-hover:border-accent\" />\n          )}\n        </div>\n        <span className=\"sr-only\">{item.label}</span>\n      </button>\n    );\n  };\n\n  return (\n    <div\n      ref={wrapperRef}\n      style={metrics ? { width: metrics.footprint } : undefined}\n      className={cn(\n        \"relative inline-flex h-12 items-center justify-end text-foreground\",\n        className,\n      )}\n    >\n      {/* ── Tooltip rail — one surface that slides + clips ── */}\n      <div className=\"pointer-events-none absolute bottom-full left-0 z-20 mb-3\">\n        <motion.div\n          ref={railRef}\n          initial={false}\n          animate={{ x: pos.x, clipPath: pos.clip, opacity: visible ? 1 : 0 }}\n          transition={{\n            opacity: { duration: 0.18, ease: [0.22, 1, 0.36, 1] },\n            x: appearing ? { duration: 0 } : SPRING_X,\n            clipPath: appearing ? { duration: 0 } : SPRING_CLIP,\n          }}\n          style={{ willChange: \"transform, clip-path, opacity\" }}\n          className=\"relative flex w-max rounded-[10px] bg-primary text-primary-foreground shadow-xl\"\n        >\n          {items.map((item, i) => (\n            <div\n              key={item.id}\n              ref={(el) => {\n                segRefs.current[i] = el;\n              }}\n              className=\"z-[1] inline-flex h-8 items-center justify-center\"\n            >\n              <div className=\"flex items-center justify-center gap-2 whitespace-nowrap px-3 text-[13px] font-medium leading-tight tracking-[-0.01em] text-primary-foreground\">\n                {item.label}\n                {item.shortcut && (\n                  <span className=\"flex items-center justify-center gap-1\">\n                    {item.shortcut.map((key, k) => (\n                      <kbd\n                        key={k}\n                        className=\"inline-flex size-5 items-center justify-center rounded-sm border border-primary-foreground/30 p-0.5 text-xs text-primary-foreground\"\n                      >\n                        {key === \"⌘\" ? (\n                          <HugeiconsIcon\n                            icon={CommandIcon}\n                            size={12}\n                            strokeWidth={2}\n                          />\n                        ) : (\n                          key\n                        )}\n                      </kbd>\n                    ))}\n                  </span>\n                )}\n              </div>\n            </div>\n          ))}\n        </motion.div>\n      </div>\n\n      {/* ── Pill — right-aligned in the constant footprint, so it can shrink\n             to the toggle without the toggle ever moving ── */}\n      <div\n        onMouseLeave={hideTooltip}\n        onBlur={(e) => {\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        <motion.div\n          className=\"relative h-8 overflow-hidden\"\n          initial={false}\n          animate={\n            metrics\n              ? {\n                  width: collapsed ? 0 : metrics.strip,\n                  opacity: collapsed ? 0 : 1,\n                }\n              : undefined\n          }\n          style={metrics ? undefined : { width: \"auto\" }}\n          transition={{\n            width: COLLAPSE_SPRING,\n            opacity: { duration: 0.18, ease: [0.22, 1, 0.36, 1] },\n          }}\n        >\n          <div\n            ref={stripRef}\n            className=\"absolute right-0 top-0 flex h-8 items-center gap-0.5 pr-0.5\"\n          >\n            {iconEntries.map(({ item, index }) => renderButton(item, index))}\n          </div>\n        </motion.div>\n\n        {/* Pinned toggle — the anchor everything opens/closes from. */}\n        {toggleEntries.map(({ item, index }) => (\n          <div key={item.id} className=\"shrink-0\">\n            {renderButton(item, index)}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport default ToolbarDock;\n",
      "type": "registry:ui",
      "target": "components/ruixen/toolbar-dock.tsx"
    }
  ]
}