{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "expandable-tabs",
  "title": "Expandable Tabs",
  "description": "A DynamicIsland-style toolbar whose icon tabs expand into actionable menu or content panels, with full keyboard and ARIA support.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ericts.com/r/use-element-size-map.json"
  ],
  "files": [
    {
      "path": "registry/base/ui/expandable-tabs.tsx",
      "content": "\"use client\";\n\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\n\nimport {\n  useElementSizeMap,\n  type ElementSize,\n} from \"@/hooks/use-element-size-map\";\nimport { cn } from \"@/lib/utils\";\n\n// Inlined so the registry item is self-contained. Strong custom ease — the\n// defaults like `ease-out` feel weak for a morphing surface like this.\nconst EASE_OUT = [0.16, 1, 0.3, 1] as const;\n\n/** A single actionable row inside a tab's menu panel. */\nexport type ExpandableTabMenuItem = {\n  id: string;\n  label: string;\n  /** Optional secondary line shown under the label. */\n  description?: string;\n  icon?: ReactNode;\n  /** Hint shown on the trailing edge (e.g. a keyboard shortcut). */\n  shortcut?: string;\n  disabled?: boolean;\n  /** Fired when this row is chosen by click, Enter, or Space. */\n  onSelect?: () => void;\n};\n\ntype ExpandableTabBase = {\n  id: string;\n  /** Shown inside the active tab and used as the trigger's accessible name. */\n  label: string;\n  icon: ReactNode;\n  disabled?: boolean;\n};\n\n/**\n * A tab is one of three shapes:\n * - `items` — opens a panel of selectable menu rows.\n * - `content` — opens a panel of arbitrary content (form, search, …).\n * - `onSelect` — no panel; fires immediately like a toolbar button.\n */\nexport type ExpandableTabItem = ExpandableTabBase &\n  (\n    | { items: ExpandableTabMenuItem[]; content?: never; onSelect?: never }\n    | { content: ReactNode; items?: never; onSelect?: never }\n    | { onSelect: () => void; items?: never; content?: never }\n  );\n\nexport type ExpandableTabsClassNames = {\n  root?: string;\n  panel?: string;\n  bar?: string;\n  tab?: string;\n  activeTab?: string;\n  icon?: string;\n  label?: string;\n  pill?: string;\n  menu?: string;\n  menuItem?: string;\n};\n\nexport interface ExpandableTabsProps\n  extends Omit<ComponentProps<\"div\">, \"defaultValue\" | \"onSelect\"> {\n  items: ExpandableTabItem[];\n  /** Open tab id, or null/undefined for the closed (bar-only) state. */\n  value?: string | null;\n  defaultValue?: string | null;\n  onValueChange?: (id: string | null) => void;\n  /** Fired when a menu row is chosen: (tabId, itemId). */\n  onSelect?: (tabId: string, itemId: string) => void;\n  /** Collapse the panel after a menu row is chosen. Default true. */\n  closeOnSelect?: boolean;\n  /** Accessible name for the toolbar. */\n  \"aria-label\"?: string;\n  classNames?: ExpandableTabsClassNames;\n}\n\ntype ButtonState = \"active\" | \"inactive\";\ntype DockButtonGeometry = { x: number; width: number };\n\n// Width/height transitions use a deterministic ease instead of a spring. Even\n// with bounce disabled, spring settling can read as a small horizontal wobble\n// when the active label also changes the bar's measured width.\nconst SHELL_TRANSITION = { duration: 0.24, ease: EASE_OUT } as const;\nconst TAB_CHANGE_TRANSITION = { duration: 0.24, ease: EASE_OUT } as const;\nconst LABEL_OPEN = { duration: 0.18, ease: EASE_OUT } as const;\n\n// Fixed bar height keeps the content panel's bottom reserve static so the open\n// height is right on the first frame. p-2 (16) + h-9 button (36).\nconst BAR_H = 52;\nconst TAB_W = 32;\nconst BAR_X = 16;\nconst BAR_GAP = 4;\nconst ROOT_BORDER = 2;\nconst PANEL_DOCK_GAP = 4;\n\n// The island grows upward from the bar, so the content emanates from the\n// trigger (bottom): it rises into place and collapses back toward the bar.\n// Clipped above the dock so rows never pass through the icon bar.\nconst CONTENT_VARIANTS: Variants = {\n  enter: { y: 8, scale: 0.98, opacity: 0, filter: \"blur(4px)\" },\n  center: { y: 0, scale: 1, opacity: 1, filter: \"blur(0px)\" },\n  exit: {\n    y: 4,\n    scale: 0.98,\n    opacity: 0,\n    filter: \"blur(4px)\",\n    transition: { duration: 0.1, ease: EASE_OUT },\n  },\n};\n\nconst REDUCED_CONTENT_VARIANTS: Variants = {\n  enter: { opacity: 0, filter: \"blur(0px)\" },\n  center: { opacity: 1, filter: \"blur(0px)\" },\n  exit: {\n    opacity: 0,\n    filter: \"blur(0px)\",\n    transition: { duration: 0.08, ease: EASE_OUT },\n  },\n};\n\nconst CONTENT_SPRING = { type: \"spring\", duration: 0.38, bounce: 0 } as const;\n\nfunction isMenuTab(\n  item: ExpandableTabItem,\n): item is ExpandableTabBase & { items: ExpandableTabMenuItem[] } {\n  return Array.isArray((item as { items?: unknown }).items);\n}\n\nfunction isActionTab(\n  item: ExpandableTabItem,\n): item is ExpandableTabBase & { onSelect: () => void } {\n  return typeof (item as { onSelect?: unknown }).onSelect === \"function\";\n}\n\n/** A tab opens a panel only when it carries menu rows or custom content. */\nfunction hasPanel(item: ExpandableTabItem) {\n  return isMenuTab(item) || \"content\" in item;\n}\n\nfunction buttonSizeId(tabId: string, state: ButtonState) {\n  return `${tabId}:${state}`;\n}\n\nfunction getButtonState(tabId: string, activeId: string | null): ButtonState {\n  return tabId === activeId ? \"active\" : \"inactive\";\n}\n\nfunction useDockGeometry(\n  items: ExpandableTabItem[],\n  activeId: string | null,\n  buttonSizes: Record<string, ElementSize>,\n) {\n  return useMemo(() => {\n    const buttonsById: Record<string, DockButtonGeometry> = {};\n    let x = BAR_X / 2;\n    let buttonsWidth = 0;\n\n    for (const item of items) {\n      const state = getButtonState(item.id, activeId);\n      const measured = buttonSizes[buttonSizeId(item.id, state)];\n      const width = measured?.width ?? TAB_W;\n\n      buttonsById[item.id] = { x, width };\n      buttonsWidth += width;\n      x += width + BAR_GAP;\n    }\n\n    return {\n      buttonsById,\n      width:\n        buttonsWidth + Math.max(0, items.length - 1) * BAR_GAP + BAR_X,\n    };\n  }, [activeId, buttonSizes, items]);\n}\n\n/**\n * Renders a tab's panel body. The same markup feeds the hidden sizer (so the\n * shell can reserve the right size before opening) and the live panel; only the\n * live copy wires up refs and handlers.\n */\nfunction PanelBody({\n  item,\n  classNames,\n  registerItemRef,\n  onItemKeyDown,\n  onItemSelect,\n}: {\n  item: ExpandableTabItem;\n  classNames?: ExpandableTabsClassNames;\n  registerItemRef?: (index: number, node: HTMLButtonElement | null) => void;\n  onItemKeyDown?: (event: KeyboardEvent<HTMLButtonElement>, index: number) => void;\n  onItemSelect?: (menuItem: ExpandableTabMenuItem) => void;\n}) {\n  if (isMenuTab(item)) {\n    return (\n      <ul\n        role=\"menu\"\n        aria-label={item.label}\n        className={cn(\"flex w-56 flex-col gap-0.5\", classNames?.menu)}\n      >\n        {item.items.map((menuItem, index) => (\n          <li key={menuItem.id} role=\"none\">\n            <button\n              type=\"button\"\n              role=\"menuitem\"\n              tabIndex={-1}\n              disabled={menuItem.disabled}\n              ref={(node) => registerItemRef?.(index, node)}\n              onClick={() => onItemSelect?.(menuItem)}\n              onKeyDown={(event) => onItemKeyDown?.(event, index)}\n              className={cn(\n                \"flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-sm outline-none transition-colors\",\n                \"text-foreground hover:bg-foreground/5 focus-visible:bg-foreground/5 active:bg-foreground/10\",\n                \"disabled:pointer-events-none disabled:opacity-40\",\n                classNames?.menuItem,\n              )}\n            >\n              {menuItem.icon ? (\n                <span className=\"grid size-4 shrink-0 place-items-center text-muted-foreground\">\n                  {menuItem.icon}\n                </span>\n              ) : null}\n              <span className=\"flex min-w-0 flex-1 flex-col\">\n                <span className=\"truncate font-medium leading-tight\">\n                  {menuItem.label}\n                </span>\n                {menuItem.description ? (\n                  <span className=\"truncate text-xs leading-tight text-muted-foreground\">\n                    {menuItem.description}\n                  </span>\n                ) : null}\n              </span>\n              {menuItem.shortcut ? (\n                <kbd className=\"ml-auto shrink-0 rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[10px] leading-none text-muted-foreground\">\n                  {menuItem.shortcut}\n                </kbd>\n              ) : null}\n            </button>\n          </li>\n        ))}\n      </ul>\n    );\n  }\n\n  // Custom-content tab: a labeled region, not a menu.\n  return (\n    <div role=\"group\" aria-label={item.label} className=\"w-max\">\n      {\"content\" in item ? item.content : null}\n    </div>\n  );\n}\n\nfunction DockButtonContent({\n  item,\n  showLabel,\n  animatedLabel = false,\n  reduce,\n  classNames,\n}: {\n  item: ExpandableTabItem;\n  showLabel: boolean;\n  animatedLabel?: boolean;\n  reduce?: boolean | null;\n  classNames?: ExpandableTabsClassNames;\n}) {\n  const labelClassName = cn(\n    \"ml-1.5 inline-block whitespace-nowrap\",\n    classNames?.label,\n  );\n\n  return (\n    <>\n      <span\n        className={cn(\"grid shrink-0 place-items-center\", classNames?.icon)}\n      >\n        {item.icon}\n      </span>\n      {showLabel ? (\n        animatedLabel ? (\n          <motion.span\n            key={item.id}\n            aria-hidden\n            initial={\n              reduce\n                ? { opacity: 1, filter: \"blur(0px)\" }\n                : { opacity: 0, filter: \"blur(3px)\" }\n            }\n            animate={{ opacity: 1, filter: \"blur(0px)\" }}\n            transition={reduce ? { duration: 0 } : LABEL_OPEN}\n            className={labelClassName}\n          >\n            {item.label}\n          </motion.span>\n        ) : (\n          <span className={labelClassName}>{item.label}</span>\n        )\n      ) : null}\n    </>\n  );\n}\n\nfunction DockMeasurers({\n  items,\n  classNames,\n  setButtonMeasureRef,\n}: {\n  items: ExpandableTabItem[];\n  classNames?: ExpandableTabsClassNames;\n  setButtonMeasureRef: (\n    id: string,\n  ) => (node: HTMLButtonElement | null) => void;\n}) {\n  return (\n    <div\n      aria-hidden\n      className=\"pointer-events-none invisible absolute left-0 top-0 flex\"\n    >\n      {items.map((item) => (\n        <div key={item.id} className=\"flex\">\n          {([\"inactive\", \"active\"] as const).map((state) => {\n            const isActive = state === \"active\";\n\n            return (\n              <button\n                key={state}\n                ref={setButtonMeasureRef(buttonSizeId(item.id, state))}\n                type=\"button\"\n                tabIndex={-1}\n                className={cn(\n                  \"relative isolate flex h-9 shrink-0 items-center justify-center overflow-hidden rounded-xl px-2 text-sm font-medium outline-none\",\n                  isActive && \"justify-start pl-2.5 pr-4\",\n                  classNames?.tab,\n                  isActive && classNames?.activeTab,\n                )}\n              >\n                <DockButtonContent\n                  item={item}\n                  showLabel={isActive}\n                  classNames={classNames}\n                />\n              </button>\n            );\n          })}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction PanelMeasurers({\n  items,\n  classNames,\n  setPanelMeasureRef,\n}: {\n  items: ExpandableTabItem[];\n  classNames?: ExpandableTabsClassNames;\n  setPanelMeasureRef: (id: string) => (node: HTMLDivElement | null) => void;\n}) {\n  return (\n    <>\n      {items.filter(hasPanel).map((item) => (\n        <div\n          key={item.id}\n          ref={setPanelMeasureRef(item.id)}\n          aria-hidden\n          className={cn(\n            \"pointer-events-none invisible absolute left-0 top-0 w-max px-2 pt-2\",\n            classNames?.panel,\n          )}\n          style={{ paddingBottom: BAR_H + PANEL_DOCK_GAP }}\n        >\n          <div className=\"w-max\">\n            <PanelBody item={item} classNames={classNames} />\n          </div>\n        </div>\n      ))}\n    </>\n  );\n}\n\nfunction DockToolbar({\n  items,\n  activeId,\n  baseId,\n  width,\n  buttonsById,\n  enabledTabIds,\n  openingFromClosed,\n  reduce,\n  ariaLabel,\n  classNames,\n  registerTabRef,\n  onActivate,\n  onKeyDown,\n}: {\n  items: ExpandableTabItem[];\n  activeId: string | null;\n  baseId: string;\n  width: number;\n  buttonsById: Record<string, DockButtonGeometry>;\n  enabledTabIds: string[];\n  openingFromClosed: boolean;\n  reduce: boolean | null;\n  ariaLabel: string;\n  classNames?: ExpandableTabsClassNames;\n  registerTabRef: (id: string, node: HTMLButtonElement | null) => void;\n  onActivate: (item: ExpandableTabItem) => void;\n  onKeyDown: (\n    event: KeyboardEvent<HTMLButtonElement>,\n    item: ExpandableTabItem,\n  ) => void;\n}) {\n  return (\n    <motion.div\n      role=\"toolbar\"\n      aria-label={ariaLabel}\n      aria-orientation=\"horizontal\"\n      initial={false}\n      animate={{ width }}\n      transition={\n        reduce || openingFromClosed ? { duration: 0 } : SHELL_TRANSITION\n      }\n      className={cn(\n        \"absolute bottom-0 left-1/2 z-20 -translate-x-1/2\",\n        classNames?.bar,\n      )}\n      style={{ height: BAR_H }}\n    >\n      {items.map((item) => {\n        const isActive = item.id === activeId;\n        const geometry = buttonsById[item.id] ?? { x: BAR_X / 2, width: TAB_W };\n        const isTabbable =\n          item.id === (activeId ?? enabledTabIds[0]) && !item.disabled;\n        const popup = isMenuTab(item)\n          ? \"menu\"\n          : \"content\" in item\n            ? \"dialog\"\n            : undefined;\n\n        return (\n          <motion.button\n            key={item.id}\n            type=\"button\"\n            disabled={item.disabled}\n            aria-haspopup={popup}\n            aria-expanded={hasPanel(item) ? isActive : undefined}\n            aria-controls={isActive ? `${baseId}-panel-${item.id}` : undefined}\n            aria-label={item.label}\n            tabIndex={isTabbable ? 0 : -1}\n            ref={(node) => registerTabRef(item.id, node)}\n            onClick={() => onActivate(item)}\n            onKeyDown={(event) => onKeyDown(event, item)}\n            initial={false}\n            animate={{ x: geometry.x, width: geometry.width }}\n            transition={\n              reduce || openingFromClosed\n                ? { duration: 0 }\n                : TAB_CHANGE_TRANSITION\n            }\n            className={cn(\n              \"absolute left-0 top-2 isolate flex h-9 shrink-0 items-center justify-center overflow-hidden rounded-xl px-2 text-sm font-medium outline-none transition-colors\",\n              \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n              \"disabled:pointer-events-none disabled:opacity-40\",\n              isActive && \"justify-start pl-2.5 pr-4\",\n              isActive\n                ? \"text-foreground\"\n                : \"text-muted-foreground hover:text-foreground\",\n              classNames?.tab,\n              isActive && classNames?.activeTab,\n            )}\n          >\n            {isActive ? (\n              <span\n                className={cn(\n                  \"absolute inset-0 -z-10 rounded-xl bg-foreground/10\",\n                  classNames?.pill,\n                )}\n              />\n            ) : null}\n            <DockButtonContent\n              item={item}\n              showLabel={isActive}\n              animatedLabel\n              reduce={reduce}\n              classNames={classNames}\n            />\n          </motion.button>\n        );\n      })}\n    </motion.div>\n  );\n}\n\nexport function ExpandableTabs({\n  items,\n  value,\n  defaultValue = null,\n  onValueChange,\n  onSelect,\n  closeOnSelect = true,\n  \"aria-label\": ariaLabel = \"Quick actions\",\n  className,\n  classNames,\n  style,\n  onKeyDown,\n  ref,\n  ...props\n}: ExpandableTabsProps) {\n  const reduce = useReducedMotion();\n  const baseId = useId();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const { setMeasureRef: setPanelMeasureRef, sizes: panelSizes } =\n    useElementSizeMap<HTMLDivElement>();\n  const { setMeasureRef: setButtonMeasureRef, sizes: buttonSizes } =\n    useElementSizeMap<HTMLButtonElement>();\n\n  const tabRefs = useRef<Record<string, HTMLButtonElement | null>>({});\n  const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  // When a panel is opened via keyboard we move focus into the first row.\n  const focusMenuOnOpen = useRef<\"first\" | \"last\" | null>(null);\n\n  const controlled = value !== undefined;\n  const [internal, setInternal] = useState<string | null>(defaultValue);\n  const activeId = controlled ? value : internal;\n  const active = items.find((item) => item.id === activeId) ?? null;\n  // An action tab (or a disabled one) never counts as visually open.\n  const visualActiveId = active && hasPanel(active) ? active.id : null;\n  const visualActive = visualActiveId ? active : null;\n  const [openingFromClosed, setOpeningFromClosed] = useState(false);\n\n  const setActive = useCallback(\n    (next: string | null) => {\n      const nextItem = items.find((item) => item.id === next) ?? null;\n      const nextVisualActiveId = nextItem && hasPanel(nextItem) ? next : null;\n      setOpeningFromClosed(\n        visualActiveId === null && nextVisualActiveId !== null,\n      );\n      if (!controlled) setInternal(next);\n      onValueChange?.(next);\n    },\n    [controlled, items, onValueChange, visualActiveId],\n  );\n\n  const enabledTabIds = items\n    .filter((item) => !item.disabled)\n    .map((item) => item.id);\n\n  const focusTab = useCallback((id: string) => {\n    tabRefs.current[id]?.focus();\n  }, []);\n\n  const enabledMenuIndexes = useCallback(\n    (item: ExpandableTabItem | null) =>\n      item && isMenuTab(item)\n        ? item.items\n            .map((menuItem, index) => (menuItem.disabled ? -1 : index))\n            .filter((index) => index >= 0)\n        : [],\n    [],\n  );\n\n  const focusMenuItem = useCallback((index: number) => {\n    menuItemRefs.current[index]?.focus();\n  }, []);\n\n  // Move focus into the menu once the panel for a keyboard-opened tab mounts.\n  useEffect(() => {\n    if (!visualActive || !isMenuTab(visualActive)) {\n      menuItemRefs.current = [];\n      return;\n    }\n\n    const intent = focusMenuOnOpen.current;\n    focusMenuOnOpen.current = null;\n\n    if (!intent) return;\n\n    const enabled = enabledMenuIndexes(visualActive);\n    if (enabled.length === 0) return;\n\n    const target = intent === \"first\" ? enabled[0] : enabled[enabled.length - 1];\n    focusMenuItem(target);\n  }, [enabledMenuIndexes, focusMenuItem, visualActive, visualActiveId]);\n\n  // Outside click / Escape closes — it behaves like an open menu.\n  useEffect(() => {\n    if (!visualActiveId) return;\n    const onPointer = (e: PointerEvent) => {\n      if (!rootRef.current?.contains(e.target as Node)) setActive(null);\n    };\n    document.addEventListener(\"pointerdown\", onPointer);\n    return () => {\n      document.removeEventListener(\"pointerdown\", onPointer);\n    };\n  }, [setActive, visualActiveId]);\n\n  const activateTab = useCallback(\n    (item: ExpandableTabItem) => {\n      if (item.disabled) return;\n\n      if (isActionTab(item)) {\n        item.onSelect();\n        setActive(null);\n        return;\n      }\n\n      setActive(item.id === visualActiveId ? null : item.id);\n    },\n    [setActive, visualActiveId],\n  );\n\n  const selectMenuItem = useCallback(\n    (tab: ExpandableTabItem, menuItem: ExpandableTabMenuItem) => {\n      if (menuItem.disabled) return;\n      menuItem.onSelect?.();\n      onSelect?.(tab.id, menuItem.id);\n      if (closeOnSelect) {\n        setActive(null);\n        focusTab(tab.id);\n      }\n    },\n    [closeOnSelect, focusTab, onSelect, setActive],\n  );\n\n  const handleTabKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLButtonElement>, item: ExpandableTabItem) => {\n      const key = event.key;\n      const currentIndex = enabledTabIds.indexOf(item.id);\n      const lastIndex = enabledTabIds.length - 1;\n\n      if (key === \"ArrowRight\") {\n        event.preventDefault();\n        const next = currentIndex >= lastIndex ? 0 : currentIndex + 1;\n        focusTab(enabledTabIds[next]);\n      } else if (key === \"ArrowLeft\") {\n        event.preventDefault();\n        const next = currentIndex <= 0 ? lastIndex : currentIndex - 1;\n        focusTab(enabledTabIds[next]);\n      } else if (key === \"Home\") {\n        event.preventDefault();\n        focusTab(enabledTabIds[0]);\n      } else if (key === \"End\") {\n        event.preventDefault();\n        focusTab(enabledTabIds[lastIndex]);\n      } else if (\n        (key === \"ArrowDown\" || key === \"ArrowUp\") &&\n        isMenuTab(item)\n      ) {\n        // Open (if needed) and dive into the menu.\n        event.preventDefault();\n        focusMenuOnOpen.current = key === \"ArrowDown\" ? \"first\" : \"last\";\n        if (item.id !== visualActiveId) {\n          setActive(item.id);\n        } else {\n          const enabled = enabledMenuIndexes(item);\n          if (enabled.length > 0) {\n            focusMenuItem(\n              key === \"ArrowDown\" ? enabled[0] : enabled[enabled.length - 1],\n            );\n          }\n        }\n      }\n    },\n    [\n      enabledMenuIndexes,\n      enabledTabIds,\n      focusMenuItem,\n      focusTab,\n      setActive,\n      visualActiveId,\n    ],\n  );\n\n  const handleMenuKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLButtonElement>, index: number) => {\n      if (!visualActive || !isMenuTab(visualActive)) return;\n      const enabled = enabledMenuIndexes(visualActive);\n      if (enabled.length === 0) return;\n\n      const pos = enabled.indexOf(index);\n      const key = event.key;\n\n      if (key === \"ArrowDown\") {\n        event.preventDefault();\n        focusMenuItem(enabled[(pos + 1) % enabled.length]);\n      } else if (key === \"ArrowUp\") {\n        event.preventDefault();\n        focusMenuItem(enabled[(pos - 1 + enabled.length) % enabled.length]);\n      } else if (key === \"Home\") {\n        event.preventDefault();\n        focusMenuItem(enabled[0]);\n      } else if (key === \"End\") {\n        event.preventDefault();\n        focusMenuItem(enabled[enabled.length - 1]);\n      } else if (key === \"Tab\") {\n        // Let focus leave naturally, but collapse the panel behind it.\n        setActive(null);\n      }\n    },\n    [enabledMenuIndexes, focusMenuItem, setActive, visualActive],\n  );\n\n  // Escape collapses any open panel (menu or custom content) and returns\n  // focus to the trigger that opened it.\n  const handleRootKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLDivElement>) => {\n      if (event.defaultPrevented) return;\n      if (event.key === \"Escape\" && visualActiveId) {\n        event.preventDefault();\n        const openId = visualActiveId;\n        setActive(null);\n        focusTab(openId);\n      }\n    },\n    [focusTab, setActive, visualActiveId],\n  );\n\n  // The root node is needed internally (outside-click detection) *and* by\n  // consumers, so the consumer's ref is merged in rather than overwritten.\n  const setRootRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      rootRef.current = node;\n\n      if (typeof ref === \"function\") {\n        ref(node);\n      } else if (ref) {\n        ref.current = node;\n      }\n    },\n    [ref],\n  );\n\n  const dock = useDockGeometry(items, visualActiveId, buttonSizes);\n  const registerTabRef = useCallback(\n    (id: string, node: HTMLButtonElement | null) => {\n      tabRefs.current[id] = node;\n    },\n    [],\n  );\n  const closedSize = {\n    width: dock.width + ROOT_BORDER,\n    height: BAR_H + ROOT_BORDER,\n  };\n\n  // The active panel drives the shell, while the dock width is derived from the\n  // measured target width of each button state. That keeps the geometry explicit:\n  // shell, dock, and buttons all animate toward the same stable numbers.\n  const activePanelSize = visualActive ? panelSizes[visualActive.id] : undefined;\n  const openSize = activePanelSize\n    ? {\n        width: Math.max(activePanelSize.width + ROOT_BORDER, closedSize.width),\n        height: Math.max(activePanelSize.height + ROOT_BORDER, closedSize.height),\n      }\n    : closedSize;\n  const targetSize = visualActive ? openSize : closedSize;\n\n  return (\n    <>\n      <div\n        {...props}\n        ref={setRootRef}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          handleRootKeyDown(event);\n        }}\n        style={{\n          ...style,\n          width: targetSize.width,\n          height: targetSize.height,\n          transformOrigin: \"bottom center\",\n        }}\n        className={cn(\"relative\", className)}\n      >\n        <PanelMeasurers\n          items={items}\n          classNames={classNames}\n          setPanelMeasureRef={setPanelMeasureRef}\n        />\n        <DockMeasurers\n          items={items}\n          classNames={classNames}\n          setButtonMeasureRef={setButtonMeasureRef}\n        />\n\n        <motion.div\n          initial={false}\n          animate={{ width: targetSize.width, height: targetSize.height }}\n          transition={\n            reduce\n              ? { duration: 0 }\n              : openingFromClosed\n                ? { width: { duration: 0 }, height: SHELL_TRANSITION }\n                : SHELL_TRANSITION\n          }\n          onAnimationComplete={() => setOpeningFromClosed(false)}\n          style={{ transformOrigin: \"bottom center\" }}\n          className={cn(\n            \"absolute bottom-0 left-1/2 -translate-x-1/2 overflow-hidden rounded-2xl border border-border bg-card\",\n            classNames?.root,\n          )}\n        >\n          <div\n            className={cn(\n              \"absolute left-0 right-0 top-0 z-10 overflow-hidden px-2 pt-2\",\n              classNames?.panel,\n            )}\n            style={{ bottom: BAR_H + PANEL_DOCK_GAP }}\n          >\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              {visualActive ? (\n                <motion.div\n                  key={visualActive.id}\n                  id={`${baseId}-panel-${visualActive.id}`}\n                  variants={\n                    reduce ? REDUCED_CONTENT_VARIANTS : CONTENT_VARIANTS\n                  }\n                  initial=\"enter\"\n                  animate=\"center\"\n                  exit=\"exit\"\n                  transition={\n                    reduce\n                      ? { duration: 0.15, ease: EASE_OUT }\n                      : openingFromClosed\n                        ? { ...CONTENT_SPRING, delay: 0.04 }\n                        : CONTENT_SPRING\n                  }\n                  className=\"w-max\"\n                  style={{\n                    transformOrigin: \"bottom center\",\n                    willChange: \"transform, opacity, filter\",\n                  }}\n                >\n                  <PanelBody\n                    item={visualActive}\n                    classNames={classNames}\n                    registerItemRef={(index, node) => {\n                      menuItemRefs.current[index] = node;\n                    }}\n                    onItemKeyDown={handleMenuKeyDown}\n                    onItemSelect={(menuItem) =>\n                      selectMenuItem(visualActive, menuItem)\n                    }\n                  />\n                </motion.div>\n              ) : null}\n            </AnimatePresence>\n          </div>\n        </motion.div>\n\n        <DockToolbar\n          items={items}\n          activeId={visualActiveId}\n          baseId={baseId}\n          width={dock.width}\n          buttonsById={dock.buttonsById}\n          enabledTabIds={enabledTabIds}\n          openingFromClosed={openingFromClosed}\n          reduce={reduce}\n          ariaLabel={ariaLabel}\n          classNames={classNames}\n          registerTabRef={registerTabRef}\n          onActivate={activateTab}\n          onKeyDown={handleTabKeyDown}\n        />\n      </div>\n    </>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/expandable-tabs.tsx"
    }
  ],
  "meta": {
    "tags": [
      "toolbar",
      "expandable",
      "menu",
      "command-bar",
      "roving-focus",
      "arrow-keys"
    ],
    "effects": [
      "shared-layout",
      "morph",
      "blur",
      "spring"
    ]
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}