Expandable Tabs

A DynamicIsland-style toolbar whose icon tabs expand into actionable menu or content panels, with full keyboard and ARIA support.

navigationshared-layout
"use client";

import {
  AnimatePresence,
  motion,
  useReducedMotion,
  type Variants,
} from "motion/react";
import {
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  type KeyboardEvent,
  type ReactNode,
} from "react";

import {
  useElementSizeMap,
  type ElementSize,
} from "@/hooks/use-element-size-map";
import { cn } from "@/lib/utils";

// Inlined so the registry item is self-contained. Strong custom ease — the
// defaults like `ease-out` feel weak for a morphing surface like this.
const EASE_OUT = [0.16, 1, 0.3, 1] as const;

/** A single actionable row inside a tab's menu panel. */
export type ExpandableTabMenuItem = {
  id: string;
  label: string;
  /** Optional secondary line shown under the label. */
  description?: string;
  icon?: ReactNode;
  /** Hint shown on the trailing edge (e.g. a keyboard shortcut). */
  shortcut?: string;
  disabled?: boolean;
  /** Fired when this row is chosen by click, Enter, or Space. */
  onSelect?: () => void;
};

type ExpandableTabBase = {
  id: string;
  /** Shown inside the active tab and used as the trigger's accessible name. */
  label: string;
  icon: ReactNode;
  disabled?: boolean;
};

/**
 * A tab is one of three shapes:
 * - `items` — opens a panel of selectable menu rows.
 * - `content` — opens a panel of arbitrary content (form, search, …).
 * - `onSelect` — no panel; fires immediately like a toolbar button.
 */
export type ExpandableTabItem = ExpandableTabBase &
  (
    | { items: ExpandableTabMenuItem[]; content?: never; onSelect?: never }
    | { content: ReactNode; items?: never; onSelect?: never }
    | { onSelect: () => void; items?: never; content?: never }
  );

export type ExpandableTabsClassNames = {
  root?: string;
  panel?: string;
  bar?: string;
  tab?: string;
  activeTab?: string;
  icon?: string;
  label?: string;
  pill?: string;
  menu?: string;
  menuItem?: string;
};

export interface ExpandableTabsProps {
  items: ExpandableTabItem[];
  /** Open tab id, or null/undefined for the closed (bar-only) state. */
  value?: string | null;
  defaultValue?: string | null;
  onValueChange?: (id: string | null) => void;
  /** Fired when a menu row is chosen: (tabId, itemId). */
  onSelect?: (tabId: string, itemId: string) => void;
  /** Collapse the panel after a menu row is chosen. Default true. */
  closeOnSelect?: boolean;
  /** Accessible name for the toolbar. */
  "aria-label"?: string;
  className?: string;
  classNames?: ExpandableTabsClassNames;
}

type ButtonState = "active" | "inactive";
type DockButtonGeometry = { x: number; width: number };

// Width/height transitions use a deterministic ease instead of a spring. Even
// with bounce disabled, spring settling can read as a small horizontal wobble
// when the active label also changes the bar's measured width.
const SHELL_TRANSITION = { duration: 0.24, ease: EASE_OUT } as const;
const TAB_CHANGE_TRANSITION = { duration: 0.24, ease: EASE_OUT } as const;
const LABEL_OPEN = { duration: 0.18, ease: EASE_OUT } as const;

// Fixed bar height keeps the content panel's bottom reserve static so the open
// height is right on the first frame. p-2 (16) + h-9 button (36).
const BAR_H = 52;
const TAB_W = 32;
const BAR_X = 16;
const BAR_GAP = 4;
const ROOT_BORDER = 2;
const PANEL_DOCK_GAP = 4;

// The island grows upward from the bar, so the content emanates from the
// trigger (bottom): it rises into place and collapses back toward the bar.
// Clipped above the dock so rows never pass through the icon bar.
const CONTENT_VARIANTS: Variants = {
  enter: { y: 8, scale: 0.98, opacity: 0, filter: "blur(4px)" },
  center: { y: 0, scale: 1, opacity: 1, filter: "blur(0px)" },
  exit: {
    y: 4,
    scale: 0.98,
    opacity: 0,
    filter: "blur(4px)",
    transition: { duration: 0.1, ease: EASE_OUT },
  },
};

const REDUCED_CONTENT_VARIANTS: Variants = {
  enter: { opacity: 0, filter: "blur(0px)" },
  center: { opacity: 1, filter: "blur(0px)" },
  exit: {
    opacity: 0,
    filter: "blur(0px)",
    transition: { duration: 0.08, ease: EASE_OUT },
  },
};

const CONTENT_SPRING = { type: "spring", duration: 0.38, bounce: 0 } as const;

function isMenuTab(
  item: ExpandableTabItem,
): item is ExpandableTabBase & { items: ExpandableTabMenuItem[] } {
  return Array.isArray((item as { items?: unknown }).items);
}

function isActionTab(
  item: ExpandableTabItem,
): item is ExpandableTabBase & { onSelect: () => void } {
  return typeof (item as { onSelect?: unknown }).onSelect === "function";
}

/** A tab opens a panel only when it carries menu rows or custom content. */
function hasPanel(item: ExpandableTabItem) {
  return isMenuTab(item) || "content" in item;
}

function buttonSizeId(tabId: string, state: ButtonState) {
  return `${tabId}:${state}`;
}

function getButtonState(tabId: string, activeId: string | null): ButtonState {
  return tabId === activeId ? "active" : "inactive";
}

function useDockGeometry(
  items: ExpandableTabItem[],
  activeId: string | null,
  buttonSizes: Record<string, ElementSize>,
) {
  return useMemo(() => {
    const buttonsById: Record<string, DockButtonGeometry> = {};
    let x = BAR_X / 2;
    let buttonsWidth = 0;

    for (const item of items) {
      const state = getButtonState(item.id, activeId);
      const measured = buttonSizes[buttonSizeId(item.id, state)];
      const width = measured?.width ?? TAB_W;

      buttonsById[item.id] = { x, width };
      buttonsWidth += width;
      x += width + BAR_GAP;
    }

    return {
      buttonsById,
      width:
        buttonsWidth + Math.max(0, items.length - 1) * BAR_GAP + BAR_X,
    };
  }, [activeId, buttonSizes, items]);
}

/**
 * Renders a tab's panel body. The same markup feeds the hidden sizer (so the
 * shell can reserve the right size before opening) and the live panel; only the
 * live copy wires up refs and handlers.
 */
function PanelBody({
  item,
  classNames,
  registerItemRef,
  onItemKeyDown,
  onItemSelect,
}: {
  item: ExpandableTabItem;
  classNames?: ExpandableTabsClassNames;
  registerItemRef?: (index: number, node: HTMLButtonElement | null) => void;
  onItemKeyDown?: (event: KeyboardEvent<HTMLButtonElement>, index: number) => void;
  onItemSelect?: (menuItem: ExpandableTabMenuItem) => void;
}) {
  if (isMenuTab(item)) {
    return (
      <ul
        role="menu"
        aria-label={item.label}
        className={cn("flex w-56 flex-col gap-0.5", classNames?.menu)}
      >
        {item.items.map((menuItem, index) => (
          <li key={menuItem.id} role="none">
            <button
              type="button"
              role="menuitem"
              tabIndex={-1}
              disabled={menuItem.disabled}
              ref={(node) => registerItemRef?.(index, node)}
              onClick={() => onItemSelect?.(menuItem)}
              onKeyDown={(event) => onItemKeyDown?.(event, index)}
              className={cn(
                "flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-sm outline-none transition-colors",
                "text-foreground hover:bg-foreground/5 focus-visible:bg-foreground/5 active:bg-foreground/10",
                "disabled:pointer-events-none disabled:opacity-40",
                classNames?.menuItem,
              )}
            >
              {menuItem.icon ? (
                <span className="grid size-4 shrink-0 place-items-center text-muted-foreground">
                  {menuItem.icon}
                </span>
              ) : null}
              <span className="flex min-w-0 flex-1 flex-col">
                <span className="truncate font-medium leading-tight">
                  {menuItem.label}
                </span>
                {menuItem.description ? (
                  <span className="truncate text-xs leading-tight text-muted-foreground">
                    {menuItem.description}
                  </span>
                ) : null}
              </span>
              {menuItem.shortcut ? (
                <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">
                  {menuItem.shortcut}
                </kbd>
              ) : null}
            </button>
          </li>
        ))}
      </ul>
    );
  }

  // Custom-content tab: a labeled region, not a menu.
  return (
    <div role="group" aria-label={item.label} className="w-max">
      {"content" in item ? item.content : null}
    </div>
  );
}

function DockButtonContent({
  item,
  showLabel,
  animatedLabel = false,
  reduce,
  classNames,
}: {
  item: ExpandableTabItem;
  showLabel: boolean;
  animatedLabel?: boolean;
  reduce?: boolean | null;
  classNames?: ExpandableTabsClassNames;
}) {
  const labelClassName = cn(
    "ml-1.5 inline-block whitespace-nowrap",
    classNames?.label,
  );

  return (
    <>
      <span
        className={cn("grid shrink-0 place-items-center", classNames?.icon)}
      >
        {item.icon}
      </span>
      {showLabel ? (
        animatedLabel ? (
          <motion.span
            key={item.id}
            aria-hidden
            initial={
              reduce
                ? { opacity: 1, filter: "blur(0px)" }
                : { opacity: 0, filter: "blur(3px)" }
            }
            animate={{ opacity: 1, filter: "blur(0px)" }}
            transition={reduce ? { duration: 0 } : LABEL_OPEN}
            className={labelClassName}
          >
            {item.label}
          </motion.span>
        ) : (
          <span className={labelClassName}>{item.label}</span>
        )
      ) : null}
    </>
  );
}

function DockMeasurers({
  items,
  classNames,
  setButtonMeasureRef,
}: {
  items: ExpandableTabItem[];
  classNames?: ExpandableTabsClassNames;
  setButtonMeasureRef: (
    id: string,
  ) => (node: HTMLButtonElement | null) => void;
}) {
  return (
    <div
      aria-hidden
      className="pointer-events-none invisible absolute left-0 top-0 flex"
    >
      {items.map((item) => (
        <div key={item.id} className="flex">
          {(["inactive", "active"] as const).map((state) => {
            const isActive = state === "active";

            return (
              <button
                key={state}
                ref={setButtonMeasureRef(buttonSizeId(item.id, state))}
                type="button"
                tabIndex={-1}
                className={cn(
                  "relative isolate flex h-9 shrink-0 items-center justify-center overflow-hidden rounded-xl px-2 text-sm font-medium outline-none",
                  isActive && "justify-start pl-2.5 pr-4",
                  classNames?.tab,
                  isActive && classNames?.activeTab,
                )}
              >
                <DockButtonContent
                  item={item}
                  showLabel={isActive}
                  classNames={classNames}
                />
              </button>
            );
          })}
        </div>
      ))}
    </div>
  );
}

function PanelMeasurers({
  items,
  classNames,
  setPanelMeasureRef,
}: {
  items: ExpandableTabItem[];
  classNames?: ExpandableTabsClassNames;
  setPanelMeasureRef: (id: string) => (node: HTMLDivElement | null) => void;
}) {
  return (
    <>
      {items.filter(hasPanel).map((item) => (
        <div
          key={item.id}
          ref={setPanelMeasureRef(item.id)}
          aria-hidden
          className={cn(
            "pointer-events-none invisible absolute left-0 top-0 w-max px-2 pt-2",
            classNames?.panel,
          )}
          style={{ paddingBottom: BAR_H + PANEL_DOCK_GAP }}
        >
          <div className="w-max">
            <PanelBody item={item} classNames={classNames} />
          </div>
        </div>
      ))}
    </>
  );
}

function DockToolbar({
  items,
  activeId,
  baseId,
  width,
  buttonsById,
  enabledTabIds,
  openingFromClosed,
  reduce,
  ariaLabel,
  classNames,
  registerTabRef,
  onActivate,
  onKeyDown,
}: {
  items: ExpandableTabItem[];
  activeId: string | null;
  baseId: string;
  width: number;
  buttonsById: Record<string, DockButtonGeometry>;
  enabledTabIds: string[];
  openingFromClosed: boolean;
  reduce: boolean | null;
  ariaLabel: string;
  classNames?: ExpandableTabsClassNames;
  registerTabRef: (id: string, node: HTMLButtonElement | null) => void;
  onActivate: (item: ExpandableTabItem) => void;
  onKeyDown: (
    event: KeyboardEvent<HTMLButtonElement>,
    item: ExpandableTabItem,
  ) => void;
}) {
  return (
    <motion.div
      role="toolbar"
      aria-label={ariaLabel}
      aria-orientation="horizontal"
      initial={false}
      animate={{ width }}
      transition={
        reduce || openingFromClosed ? { duration: 0 } : SHELL_TRANSITION
      }
      className={cn(
        "absolute bottom-0 left-1/2 z-20 -translate-x-1/2",
        classNames?.bar,
      )}
      style={{ height: BAR_H }}
    >
      {items.map((item) => {
        const isActive = item.id === activeId;
        const geometry = buttonsById[item.id] ?? { x: BAR_X / 2, width: TAB_W };
        const isTabbable =
          item.id === (activeId ?? enabledTabIds[0]) && !item.disabled;
        const popup = isMenuTab(item)
          ? "menu"
          : "content" in item
            ? "dialog"
            : undefined;

        return (
          <motion.button
            key={item.id}
            type="button"
            disabled={item.disabled}
            aria-haspopup={popup}
            aria-expanded={hasPanel(item) ? isActive : undefined}
            aria-controls={isActive ? `${baseId}-panel-${item.id}` : undefined}
            aria-label={item.label}
            tabIndex={isTabbable ? 0 : -1}
            ref={(node) => registerTabRef(item.id, node)}
            onClick={() => onActivate(item)}
            onKeyDown={(event) => onKeyDown(event, item)}
            initial={false}
            animate={{ x: geometry.x, width: geometry.width }}
            transition={
              reduce || openingFromClosed
                ? { duration: 0 }
                : TAB_CHANGE_TRANSITION
            }
            className={cn(
              "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",
              "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
              "disabled:pointer-events-none disabled:opacity-40",
              isActive && "justify-start pl-2.5 pr-4",
              isActive
                ? "text-foreground"
                : "text-muted-foreground hover:text-foreground",
              classNames?.tab,
              isActive && classNames?.activeTab,
            )}
          >
            {isActive ? (
              <span
                className={cn(
                  "absolute inset-0 -z-10 rounded-xl bg-foreground/10",
                  classNames?.pill,
                )}
              />
            ) : null}
            <DockButtonContent
              item={item}
              showLabel={isActive}
              animatedLabel
              reduce={reduce}
              classNames={classNames}
            />
          </motion.button>
        );
      })}
    </motion.div>
  );
}

export function ExpandableTabs({
  items,
  value,
  defaultValue = null,
  onValueChange,
  onSelect,
  closeOnSelect = true,
  "aria-label": ariaLabel = "Quick actions",
  className,
  classNames,
}: ExpandableTabsProps) {
  const reduce = useReducedMotion();
  const baseId = useId();
  const rootRef = useRef<HTMLDivElement>(null);
  const { setMeasureRef: setPanelMeasureRef, sizes: panelSizes } =
    useElementSizeMap<HTMLDivElement>();
  const { setMeasureRef: setButtonMeasureRef, sizes: buttonSizes } =
    useElementSizeMap<HTMLButtonElement>();

  const tabRefs = useRef<Record<string, HTMLButtonElement | null>>({});
  const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);
  // When a panel is opened via keyboard we move focus into the first row.
  const focusMenuOnOpen = useRef<"first" | "last" | null>(null);

  const controlled = value !== undefined;
  const [internal, setInternal] = useState<string | null>(defaultValue);
  const activeId = controlled ? value : internal;
  const active = items.find((item) => item.id === activeId) ?? null;
  // An action tab (or a disabled one) never counts as visually open.
  const visualActiveId = active && hasPanel(active) ? active.id : null;
  const visualActive = visualActiveId ? active : null;
  const [openingFromClosed, setOpeningFromClosed] = useState(false);

  const setActive = useCallback(
    (next: string | null) => {
      const nextItem = items.find((item) => item.id === next) ?? null;
      const nextVisualActiveId = nextItem && hasPanel(nextItem) ? next : null;
      setOpeningFromClosed(
        visualActiveId === null && nextVisualActiveId !== null,
      );
      if (!controlled) setInternal(next);
      onValueChange?.(next);
    },
    [controlled, items, onValueChange, visualActiveId],
  );

  const enabledTabIds = items
    .filter((item) => !item.disabled)
    .map((item) => item.id);

  const focusTab = useCallback((id: string) => {
    tabRefs.current[id]?.focus();
  }, []);

  const enabledMenuIndexes = useCallback(
    (item: ExpandableTabItem | null) =>
      item && isMenuTab(item)
        ? item.items
            .map((menuItem, index) => (menuItem.disabled ? -1 : index))
            .filter((index) => index >= 0)
        : [],
    [],
  );

  const focusMenuItem = useCallback((index: number) => {
    menuItemRefs.current[index]?.focus();
  }, []);

  // Move focus into the menu once the panel for a keyboard-opened tab mounts.
  useEffect(() => {
    if (!visualActive || !isMenuTab(visualActive)) {
      menuItemRefs.current = [];
      return;
    }

    const intent = focusMenuOnOpen.current;
    focusMenuOnOpen.current = null;

    if (!intent) return;

    const enabled = enabledMenuIndexes(visualActive);
    if (enabled.length === 0) return;

    const target = intent === "first" ? enabled[0] : enabled[enabled.length - 1];
    focusMenuItem(target);
  }, [enabledMenuIndexes, focusMenuItem, visualActive, visualActiveId]);

  // Outside click / Escape closes — it behaves like an open menu.
  useEffect(() => {
    if (!visualActiveId) return;
    const onPointer = (e: PointerEvent) => {
      if (!rootRef.current?.contains(e.target as Node)) setActive(null);
    };
    document.addEventListener("pointerdown", onPointer);
    return () => {
      document.removeEventListener("pointerdown", onPointer);
    };
  }, [setActive, visualActiveId]);

  const activateTab = useCallback(
    (item: ExpandableTabItem) => {
      if (item.disabled) return;

      if (isActionTab(item)) {
        item.onSelect();
        setActive(null);
        return;
      }

      setActive(item.id === visualActiveId ? null : item.id);
    },
    [setActive, visualActiveId],
  );

  const selectMenuItem = useCallback(
    (tab: ExpandableTabItem, menuItem: ExpandableTabMenuItem) => {
      if (menuItem.disabled) return;
      menuItem.onSelect?.();
      onSelect?.(tab.id, menuItem.id);
      if (closeOnSelect) {
        setActive(null);
        focusTab(tab.id);
      }
    },
    [closeOnSelect, focusTab, onSelect, setActive],
  );

  const handleTabKeyDown = useCallback(
    (event: KeyboardEvent<HTMLButtonElement>, item: ExpandableTabItem) => {
      const key = event.key;
      const currentIndex = enabledTabIds.indexOf(item.id);
      const lastIndex = enabledTabIds.length - 1;

      if (key === "ArrowRight") {
        event.preventDefault();
        const next = currentIndex >= lastIndex ? 0 : currentIndex + 1;
        focusTab(enabledTabIds[next]);
      } else if (key === "ArrowLeft") {
        event.preventDefault();
        const next = currentIndex <= 0 ? lastIndex : currentIndex - 1;
        focusTab(enabledTabIds[next]);
      } else if (key === "Home") {
        event.preventDefault();
        focusTab(enabledTabIds[0]);
      } else if (key === "End") {
        event.preventDefault();
        focusTab(enabledTabIds[lastIndex]);
      } else if (
        (key === "ArrowDown" || key === "ArrowUp") &&
        isMenuTab(item)
      ) {
        // Open (if needed) and dive into the menu.
        event.preventDefault();
        focusMenuOnOpen.current = key === "ArrowDown" ? "first" : "last";
        if (item.id !== visualActiveId) {
          setActive(item.id);
        } else {
          const enabled = enabledMenuIndexes(item);
          if (enabled.length > 0) {
            focusMenuItem(
              key === "ArrowDown" ? enabled[0] : enabled[enabled.length - 1],
            );
          }
        }
      }
    },
    [
      enabledMenuIndexes,
      enabledTabIds,
      focusMenuItem,
      focusTab,
      setActive,
      visualActiveId,
    ],
  );

  const handleMenuKeyDown = useCallback(
    (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
      if (!visualActive || !isMenuTab(visualActive)) return;
      const enabled = enabledMenuIndexes(visualActive);
      if (enabled.length === 0) return;

      const pos = enabled.indexOf(index);
      const key = event.key;

      if (key === "ArrowDown") {
        event.preventDefault();
        focusMenuItem(enabled[(pos + 1) % enabled.length]);
      } else if (key === "ArrowUp") {
        event.preventDefault();
        focusMenuItem(enabled[(pos - 1 + enabled.length) % enabled.length]);
      } else if (key === "Home") {
        event.preventDefault();
        focusMenuItem(enabled[0]);
      } else if (key === "End") {
        event.preventDefault();
        focusMenuItem(enabled[enabled.length - 1]);
      } else if (key === "Tab") {
        // Let focus leave naturally, but collapse the panel behind it.
        setActive(null);
      }
    },
    [enabledMenuIndexes, focusMenuItem, setActive, visualActive],
  );

  // Escape collapses any open panel (menu or custom content) and returns
  // focus to the trigger that opened it.
  const handleRootKeyDown = useCallback(
    (event: KeyboardEvent<HTMLDivElement>) => {
      if (event.key === "Escape" && visualActiveId) {
        event.preventDefault();
        const openId = visualActiveId;
        setActive(null);
        focusTab(openId);
      }
    },
    [focusTab, setActive, visualActiveId],
  );

  const dock = useDockGeometry(items, visualActiveId, buttonSizes);
  const registerTabRef = useCallback(
    (id: string, node: HTMLButtonElement | null) => {
      tabRefs.current[id] = node;
    },
    [],
  );
  const closedSize = {
    width: dock.width + ROOT_BORDER,
    height: BAR_H + ROOT_BORDER,
  };

  // The active panel drives the shell, while the dock width is derived from the
  // measured target width of each button state. That keeps the geometry explicit:
  // shell, dock, and buttons all animate toward the same stable numbers.
  const activePanelSize = visualActive ? panelSizes[visualActive.id] : undefined;
  const openSize = activePanelSize
    ? {
        width: Math.max(activePanelSize.width + ROOT_BORDER, closedSize.width),
        height: Math.max(activePanelSize.height + ROOT_BORDER, closedSize.height),
      }
    : closedSize;
  const targetSize = visualActive ? openSize : closedSize;

  return (
    <>
      <div
        ref={rootRef}
        onKeyDown={handleRootKeyDown}
        style={{
          width: targetSize.width,
          height: targetSize.height,
          transformOrigin: "bottom center",
        }}
        className={cn("relative", className)}
      >
        <PanelMeasurers
          items={items}
          classNames={classNames}
          setPanelMeasureRef={setPanelMeasureRef}
        />
        <DockMeasurers
          items={items}
          classNames={classNames}
          setButtonMeasureRef={setButtonMeasureRef}
        />

        <motion.div
          initial={false}
          animate={{ width: targetSize.width, height: targetSize.height }}
          transition={
            reduce
              ? { duration: 0 }
              : openingFromClosed
                ? { width: { duration: 0 }, height: SHELL_TRANSITION }
                : SHELL_TRANSITION
          }
          onAnimationComplete={() => setOpeningFromClosed(false)}
          style={{ transformOrigin: "bottom center" }}
          className={cn(
            "absolute bottom-0 left-1/2 -translate-x-1/2 overflow-hidden rounded-2xl border border-border bg-card",
            classNames?.root,
          )}
        >
          <div
            className={cn(
              "absolute left-0 right-0 top-0 z-10 overflow-hidden px-2 pt-2",
              classNames?.panel,
            )}
            style={{ bottom: BAR_H + PANEL_DOCK_GAP }}
          >
            <AnimatePresence mode="popLayout" initial={false}>
              {visualActive ? (
                <motion.div
                  key={visualActive.id}
                  id={`${baseId}-panel-${visualActive.id}`}
                  variants={
                    reduce ? REDUCED_CONTENT_VARIANTS : CONTENT_VARIANTS
                  }
                  initial="enter"
                  animate="center"
                  exit="exit"
                  transition={
                    reduce
                      ? { duration: 0.15, ease: EASE_OUT }
                      : openingFromClosed
                        ? { ...CONTENT_SPRING, delay: 0.04 }
                        : CONTENT_SPRING
                  }
                  className="w-max"
                  style={{
                    transformOrigin: "bottom center",
                    willChange: "transform, opacity, filter",
                  }}
                >
                  <PanelBody
                    item={visualActive}
                    classNames={classNames}
                    registerItemRef={(index, node) => {
                      menuItemRefs.current[index] = node;
                    }}
                    onItemKeyDown={handleMenuKeyDown}
                    onItemSelect={(menuItem) =>
                      selectMenuItem(visualActive, menuItem)
                    }
                  />
                </motion.div>
              ) : null}
            </AnimatePresence>
          </div>
        </motion.div>

        <DockToolbar
          items={items}
          activeId={visualActiveId}
          baseId={baseId}
          width={dock.width}
          buttonsById={dock.buttonsById}
          enabledTabIds={enabledTabIds}
          openingFromClosed={openingFromClosed}
          reduce={reduce}
          ariaLabel={ariaLabel}
          classNames={classNames}
          registerTabRef={registerTabRef}
          onActivate={activateTab}
          onKeyDown={handleTabKeyDown}
        />
      </div>
    </>
  );
}

Installation

pnpm dlx shadcn@latest add @ericts/expandable-tabs