{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ribbon",
  "type": "registry:ui",
  "title": "Ribbon",
  "description": "Fluent 2-styled, Office-like Ribbon — the kit's flagship composite, with two layouts from one tree. **single-line** (Word's 'Faixa de Opções de Linha Única') composes the kit Tabs, Toolbar (APG roving-tabindex), and the headless Overflow priority system so the lowest-priority commands drop into a '…' menu as the row shrinks; each RibbonItem renders two ways at once (an icon in the bar and an icon + full-label row in the menu, grouped by source group). **classic** (Word's two-row band) swaps Overflow for the headless GroupCollapse manager: as the ~96px band shrinks, whole groups collapse — highest collapsePriority first — to a dropdown button whose Popover flyout holds the group's same children, then the band falls back to horizontal scroll. New classic form parts RibbonLargeButton / RibbonRow / RibbonColumn compose group content; a `layouts` escape hatch on RibbonItem/RibbonGroup shows a command in only one layout. Controlled/uncontrolled tab selection plus a `collapsed` (tabs-only) axis; unknown `layout` values degrade to single-line. Custom overflowRender lets split buttons/combos supply their own menu form; focus follows a command into the '…' trigger (single-line) or its group's dropdown button (classic) when it overflows/collapses. The `layout` axis is controllable (`layout`/`defaultLayout`/`onLayoutChange`), as is `autoAdjust` (`autoAdjust`/`defaultAutoAdjust`/`onAutoAdjustChange`); the optional RibbonLayoutSwitcher reproduces Word's far-right display-options chevron (Clássica / Linha Única + Sempre mostrar / Apenas as guias / Ajustar automaticamente), pinned via RibbonTabList's `actions` slot.",
  "author": "graundtech <https://github.com/graundtech/fluent2-react-kit>",
  "dependencies": [
    "@base-ui/react",
    "@fluentui/react-icons"
  ],
  "registryDependencies": [
    "https://fluent2-react-kit.graund.io/r/utils.json",
    "https://fluent2-react-kit.graund.io/r/tabs.json",
    "https://fluent2-react-kit.graund.io/r/toolbar.json",
    "https://fluent2-react-kit.graund.io/r/overflow.json",
    "https://fluent2-react-kit.graund.io/r/dropdown-menu.json",
    "https://fluent2-react-kit.graund.io/r/popover.json",
    "https://fluent2-react-kit.graund.io/r/ribbon-collapse.json"
  ],
  "files": [
    {
      "path": "components/ui/ribbon.tsx",
      "type": "registry:ui",
      "target": "components/ui/ribbon.tsx",
      "content": "\"use client\";\n\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport type {\n  ComponentProps,\n  CSSProperties,\n  ReactElement,\n  ReactNode,\n  Ref,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"./dropdown-menu\";\nimport {\n  Overflow,\n  OverflowDivider,\n  OverflowItem,\n  useIsOverflowGroupVisible,\n  useIsOverflowItemVisible,\n  useOverflowMenu,\n} from \"./overflow\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"./popover\";\nimport {\n  CollapseGroup,\n  GroupCollapse,\n  useGroupMode,\n  useIsScrollMode,\n} from \"./ribbon-collapse\";\nimport type {\n  CollapseGetGap,\n  CollapseGetOverflowSize,\n  CollapseGetSize,\n} from \"./ribbon-collapse\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"./tabs\";\nimport { Toolbar, ToolbarButton, ToolbarSeparator } from \"./toolbar\";\n\n/**\n * Ribbon — the kit's flagship composite: an Office-style **single-line** Ribbon\n * (Word Online's \"Faixa de Opções de Linha Única\"). It composes three primitives\n * that already exist in the kit — `Tabs` (the guide strip), `Toolbar` (the\n * command row, APG roving-tabindex), and the headless `Overflow` priority system\n * (which drops the lowest-priority commands into a \"…\" menu as the row shrinks) —\n * and adds the **item model** that makes every command render two ways at once:\n * an icon control in the bar, and an icon + full-label row inside the overflow\n * menu, grouped under a header named after its source group. That dual\n * presentation is the behavioral heart captured live from Word Online in\n * `docs/design/ribbon-behavior-spec.md` (\"Single-line mode\").\n *\n * ## Licensing / provenance\n * Fluent 2 is a **visual/behavioral reference only** (conventions §0). No Fluent\n * UI source was consulted; the contract comes from the spec doc + kit tokens.\n *\n * ## Part family (shadcn compound style)\n * | Part                  | Renders over            | Role                                            |\n * | --------------------- | ----------------------- | ----------------------------------------------- |\n * | `Ribbon`              | kit `Tabs` (Root)       | root: tab + collapse state + context            |\n * | `RibbonTabList`       | `TabsList`              | the guide strip                                 |\n * | `RibbonTab`           | `TabsTrigger`           | one guide (stamps the id the row is labelled by)|\n * | `RibbonContent`       | `TabsContent`           | one tab's single-line command row (Overflow+Toolbar) |\n * | `RibbonGroup`         | context + `OverflowDivider` | a logical command group + its trailing divider |\n * | `RibbonItem`          | `OverflowItem` + registry | one command: bar-form child + menu-form metadata |\n * | `RibbonOverflowMenu`  | `useOverflowMenu` + `DropdownMenu` | the \"…\" trigger + grouped menu (auto-rendered) |\n * | `RibbonSeparator`     | `ToolbarSeparator`      | a standalone divider outside a group            |\n *\n * A consumer only writes `Ribbon` → `RibbonTabList`/`RibbonTab` +\n * `RibbonContent` → `RibbonGroup` → `RibbonItem`. `RibbonContent` wires the\n * `Overflow` provider, the measured `Toolbar`, the item registry, **and** the\n * `RibbonOverflowMenu` for you (it appends the trigger after your groups) — this\n * productizes exactly the wiring proven in `apps/demo/app/preview/overflow`.\n *\n * ## The dual-presentation item model\n * `RibbonItem` does two things:\n * 1. wraps its `children` (the bar-form control — a `ToolbarButton`, `Toggle`,\n *    `SplitButton`, dropdown trigger, …) in an `OverflowItem`, so the shared\n *    priority manager can hide it (via CSS `display:none`, never unmount) when\n *    the row is too narrow; and\n * 2. registers `{ id, label, icon, groupId, groupLabel, overflowRender?, onSelect? }`\n *    in a `RibbonContent`-scoped registry, so `RibbonOverflowMenu` can render the\n *    **hidden** ones as menu rows. The default menu form is a `DropdownMenuItem`\n *    (icon + `label`, firing `onSelect`); a complex control supplies its own via\n *    `overflowRender` (e.g. a split button → a submenu, a combo → a labeled\n *    field). The container decides the form from context, not the item's props —\n *    exactly the spec's \"API implications\" §1.\n *\n * ## Two layouts, one tree (v2 — the classic band)\n * The SAME `RibbonGroup`/`RibbonItem` tree renders in both layouts; the form\n * parts adapt to the root `layout`. This is the v2-plan \"one tree, two layouts +\n * escape hatch\" decision.\n * - **`layout=\"single-line\"`** (default) — the v1 command row above: `Overflow`\n *   priority manager + the \"…\" menu. Unchanged; the v1 e2e suite is its\n *   regression guard.\n * - **`layout=\"classic\"`** — Word's two-row **classic band** (~96px content\n *   area). `RibbonContent` swaps the `Overflow` row for a `GroupCollapse`\n *   provider (the headless C1 group-collapse manager) wrapping the SAME\n *   `Toolbar`, and each `RibbonGroup` renders its classic anatomy: a horizontal\n *   cluster of controls over a centered muted group label, an optional\n *   dialog-launcher (↘), and a trailing hairline. When the band can't fit,\n *   whole groups collapse — highest `collapsePriority` first — to a single\n *   dropdown button whose flyout (kit `Popover`) holds the group's SAME\n *   children; when every group is collapsed and it still overflows, the band\n *   reports `scrollMode` (C1 stamps `data-scroll-mode`; the scroll UI is C3).\n *   There is **no \"…\" overflow menu** in classic — Word classic has none; its\n *   fallback is scroll. **Unknown `layout` values degrade to single-line** so\n *   forward-compat callers never break.\n *\n * ### Escape hatch — per-layout content (`layouts`)\n * Word genuinely shows different command sets per layout, so both `RibbonItem`\n * and `RibbonGroup` accept `layouts?: (\"single-line\" | \"classic\")[]`: when set\n * and the active layout isn't listed, the part renders nothing (and, in\n * single-line, does not register in the overflow/menu registry). Default: both.\n *\n * ### Collapsed-flyout children placement (design note)\n * A group's children are a single React subtree that lives in exactly ONE place\n * per mode: inside the expanded shell when the group is expanded, and inside the\n * `Popover` flyout when the group is collapsed (and the flyout is open). The two\n * form SHELLS both stay mounted for measurement (C1's model), but the children\n * are never double-rendered — the collapsed shell is a fixed icon+label+chevron\n * button whose size is independent of the children. Flipping mode therefore\n * **remounts** the children (they move between the band and the portal); ribbon\n * controls are stateless commands, so this is acceptable (v2-plan design risk).\n *\n * ## The other two API axes (mirroring Word's own switcher)\n * - **`collapsed`** — Word's \"Mostrar apenas as guias\" (tabs-only). A\n *   controlled/uncontrolled pair (`collapsed` / `defaultCollapsed` /\n *   `onCollapsedChange`): when collapsed, the command rows are hidden (via CSS,\n *   not unmounted — see `RibbonContent`'s `keepMounted` note); activating **any**\n *   tab un-collapses (Word's behavior). Applies to both layouts. **Divergence\n *   from desktop Office:** desktop shows the active tab's ribbon as a temporary\n *   *overlay flyout* on click while staying collapsed; v1 simply un-collapses\n *   (no overlay), matching the simpler web model.\n * - **`autoAdjust`** — Word's \"Ajustar automaticamente\", the **classic** adaptive\n *   resize toggle. `true` (default) is the staged group collapse then scroll\n *   fallback; `false` skips group collapse entirely — every group stays expanded\n *   and the band goes **straight to the horizontal-scroll UI** the moment it\n *   overflows (implemented by not mounting the `GroupCollapse` provider and\n *   letting the always-scrollable band's clip detection drive the edge arrows).\n *   **Ignored in single-line** (Word only exposes it for classic; the single-line\n *   row always adapts via `Overflow`).\n *\n * ## Classic fallbacks (C3)\n * - **Horizontal scroll** — when every group has collapsed and the band still\n *   overflows (`useIsScrollMode()`), or always once `autoAdjust={false}`, the band\n *   scrolls horizontally (programmatic; no native scrollbar — Word's model) with\n *   `‹`/`›` edge arrow buttons that appear only when there is clipped content on\n *   that side. The arrows are **pointer affordances** rendered as absolute\n *   siblings *outside* the `Toolbar` (so Base UI's roving tabindex never owns\n *   them) and taken out of the tab order (`tabIndex={-1}`); keyboard users move\n *   through commands with the toolbar's roving focus and the browser scrolls the\n *   focused item into view natively (the band is a real scroll container even\n *   though its overflow is clipped). Programmatic scroll respects\n *   `prefers-reduced-motion` (`behavior:\"auto\"` instead of `\"smooth\"`).\n * - **Tab-strip overflow** — the `RibbonTabList` reuses the v1 `Overflow`\n *   machinery: each `RibbonTab` is an `OverflowItem` (its child is cloned, no\n *   wrapper element, so `TabsList`'s roving + sliding indicator keep working) and\n *   a `⌄` chevron menu lists the hidden (trailing) tabs; selecting one activates\n *   it. The active tab is pinned dynamically so it can never fold. Applies to\n *   **both** layouts (Word folds tabs in single-line too).\n *\n * ## Accessibility contract\n * There is **no ARIA \"ribbon\" pattern** — the composite is APG **Tabs** (from kit\n * `Tabs`) over APG **Toolbar** (from kit `Toolbar`). Each command row is a\n * `role=\"toolbar\"` labelled by its owning tab via `aria-labelledby` (reusing the\n * tab's own id — the same id Base UI's `Tabs` already links the panel to, so no\n * new wiring is invented). The \"…\" trigger is an icon-only button and carries an\n * `aria-label` announcing the overflow count. Overflowed items are `aria-hidden`\n * in the row (the `Overflow` module sets this) and instead offered in the menu.\n *\n * ### Focus preservation on overflow (phase-2 handoff)\n * When the item holding focus is pushed into the \"…\" menu (its element becomes\n * `display:none`), focus would otherwise fall to `document.body`. `RibbonItem`\n * detects that transition — it snapshots, *during the render that flips it\n * hidden* (pre-commit DOM, so focus is still on it), whether it holds focus — and\n * on the following effect moves focus to the \"…\" trigger (which has, in the same\n * commit, become visible because something overflowed).\n *\n * ## Styling\n * Token-driven only (conventions §3). The row is a transparent surface on\n * `--background` with a `border-b` hairline — Word's ribbon band reads as chrome.\n * No new tokens. The Fluent tab underline comes from kit `Tabs`, untouched.\n *\n * ## `\"use client\"` — required\n * Owns React context, the item registry store, refs, and focus effects; also\n * pulls in client-only primitives (`Tabs`, `DropdownMenu`, `Overflow`).\n */\n\n/* -------------------------------------------------------------------------- */\n/* Small ref helpers (local, no external dep — mirrors overflow.tsx)          */\n/* -------------------------------------------------------------------------- */\n\ntype PossibleRef<T> = Ref<T> | undefined;\n\nfunction assignRef<T>(ref: PossibleRef<T>, value: T | null) {\n  if (typeof ref === \"function\") ref(value);\n  else if (ref) (ref as { current: T | null }).current = value;\n}\n\nfunction mergeRefs<T>(...refs: PossibleRef<T>[]) {\n  return (value: T | null) => {\n    for (const ref of refs) assignRef(ref, value);\n  };\n}\n\n/** In React 19 a JSX `ref` lives on `element.props.ref`. */\nfunction getChildRef(element: ReactElement): PossibleRef<HTMLElement> {\n  return (element.props as { ref?: PossibleRef<HTMLElement> }).ref;\n}\n\n// useLayoutEffect on the client, useEffect on the server (avoids the SSR warning\n// — the same isomorphic pattern overflow.tsx uses).\nconst useLayoutEffectSafe =\n  typeof document !== \"undefined\" ? useLayoutEffect : useEffect;\n\n/* -------------------------------------------------------------------------- */\n/* Item registry (RibbonContent-scoped store for the overflow menu)           */\n/* -------------------------------------------------------------------------- */\n\n/** Metadata a `RibbonItem` publishes so the overflow menu can render it. */\nexport interface RibbonItemMeta {\n  id: string;\n  label: string;\n  icon?: ReactNode;\n  groupId?: string;\n  groupLabel?: string;\n  /** Custom menu form (split-button submenu, combo field, …). */\n  overflowRender?: () => ReactNode;\n  /** Fired by the *default* menu form (a `DropdownMenuItem`). */\n  onSelect?: () => void;\n}\n\ninterface RibbonRegistry {\n  register: (meta: RibbonItemMeta) => void;\n  unregister: (id: string) => void;\n  subscribe: (listener: () => void) => () => void;\n  /** Registered items in source (registration) order. */\n  getSnapshot: () => RibbonItemMeta[];\n  getServerSnapshot: () => RibbonItemMeta[];\n}\n\nconst EMPTY_META: RibbonItemMeta[] = [];\n\nfunction createRibbonRegistry(): RibbonRegistry {\n  const items = new Map<string, RibbonItemMeta>();\n  const listeners = new Set<() => void>();\n  let snapshot: RibbonItemMeta[] = EMPTY_META;\n\n  function rebuild() {\n    // New array identity only on membership change → useSyncExternalStore stable.\n    snapshot = [...items.values()];\n    for (const listener of listeners) listener();\n  }\n\n  return {\n    register(meta) {\n      const existing = items.get(meta.id);\n      if (existing) {\n        // Same membership: mutate in place so an open menu reads fresh\n        // icon/label/closures on its next natural render, WITHOUT a new snapshot\n        // identity (which would loop, since icon/overflowRender/onSelect get new\n        // identities every parent render).\n        Object.assign(existing, meta);\n        return;\n      }\n      items.set(meta.id, { ...meta });\n      rebuild();\n    },\n    unregister(id) {\n      if (items.delete(id)) rebuild();\n    },\n    subscribe(listener) {\n      listeners.add(listener);\n      return () => listeners.delete(listener);\n    },\n    getSnapshot() {\n      return snapshot;\n    },\n    getServerSnapshot() {\n      return EMPTY_META;\n    },\n  };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Contexts                                                                    */\n/* -------------------------------------------------------------------------- */\n\ntype RibbonLayout = \"single-line\" | \"classic\";\n\n/** Normalise any layout input to a known `RibbonLayout` (unknown → single-line). */\nfunction normalizeLayout(value: RibbonLayout | (string & {})): RibbonLayout {\n  return value === \"classic\" ? \"classic\" : \"single-line\";\n}\n\ninterface RibbonContextValue {\n  layout: RibbonLayout;\n  collapsed: boolean;\n  /**\n   * Adaptive resize in **classic** (Word's \"Ajustar automaticamente\"). `true`\n   * (default) = staged group collapse then scroll; `false` = no collapse,\n   * straight to the scroll UI when the band overflows. Ignored in single-line.\n   */\n  autoAdjust: boolean;\n  /** The currently-selected tab value (so `RibbonTab` can pin the active tab). */\n  activeValue?: string;\n  /** Activate a tab by value (used by the tab-strip overflow menu); un-collapses. */\n  selectValue: (value: string) => void;\n  /** Un-collapse when a tab is activated (Word behavior). */\n  onTabActivate: () => void;\n  /** Deterministic tab-element id for a value; the row is labelled by it. */\n  tabId: (value: unknown) => string;\n  /** Set the layout (used by `RibbonLayoutSwitcher`; honours controlled `layout`). */\n  setLayout: (layout: RibbonLayout) => void;\n  /** Set the tabs-only axis (used by `RibbonLayoutSwitcher`; honours controlled `collapsed`). */\n  setCollapsed: (collapsed: boolean) => void;\n  /** Set the classic adaptive-resize axis (used by `RibbonLayoutSwitcher`; honours controlled `autoAdjust`). */\n  setAutoAdjust: (autoAdjust: boolean) => void;\n}\n\nconst RibbonContext = createContext<RibbonContextValue | null>(null);\n\nfunction useRibbonContext(): RibbonContextValue {\n  const ctx = useContext(RibbonContext);\n  if (!ctx) {\n    throw new Error(\"Ribbon parts must be rendered inside a <Ribbon>.\");\n  }\n  return ctx;\n}\n\ninterface RibbonContentContextValue {\n  registry: RibbonRegistry;\n  /** Set by `RibbonOverflowMenu`; read by `RibbonItem`'s focus effect. */\n  overflowTriggerRef: { current: HTMLElement | null };\n}\n\nconst RibbonContentContext = createContext<RibbonContentContextValue | null>(\n  null\n);\n\nfunction useRibbonContentContext(): RibbonContentContextValue {\n  const ctx = useContext(RibbonContentContext);\n  if (!ctx) {\n    throw new Error(\n      \"RibbonGroup / RibbonItem / RibbonOverflowMenu must be inside a <RibbonContent>.\"\n    );\n  }\n  return ctx;\n}\n\ninterface RibbonGroupContextValue {\n  groupId?: string;\n  groupLabel?: string;\n}\n\nconst RibbonGroupContext = createContext<RibbonGroupContextValue>({});\n\n/**\n * `RibbonTabList`-scoped registry, mirroring the `RibbonContent` item registry:\n * each `RibbonTab` publishes `{ id: value, label, icon }` so the tab-strip\n * overflow menu can list the tabs that folded behind its chevron.\n */\ninterface RibbonTabListContextValue {\n  registry: RibbonRegistry;\n}\n\nconst RibbonTabListContext = createContext<RibbonTabListContextValue | null>(\n  null\n);\n\n/* -------------------------------------------------------------------------- */\n/* Controlled/uncontrolled helper                                              */\n/* -------------------------------------------------------------------------- */\n\nfunction useControllableState<T>(\n  controlled: T | undefined,\n  defaultValue: T,\n  onChange?: (value: T) => void\n): [T, (value: T) => void] {\n  const [uncontrolled, setUncontrolled] = useState<T>(defaultValue);\n  const isControlled = controlled !== undefined;\n  const value = isControlled ? controlled : uncontrolled;\n  const setValue = useCallback(\n    (next: T) => {\n      if (!isControlled) setUncontrolled(next);\n      onChange?.(next);\n    },\n    [isControlled, onChange]\n  );\n  return [value, setValue];\n}\n\n/* -------------------------------------------------------------------------- */\n/* Ribbon (root)                                                               */\n/* -------------------------------------------------------------------------- */\n\nexport interface RibbonProps\n  extends Omit<ComponentProps<typeof Tabs>, \"value\" | \"defaultValue\"> {\n  /**\n   * `\"single-line\"` or `\"classic\"` (Word's two-row band) — **controlled**. Unknown\n   * values degrade to single-line. Pair with `onLayoutChange` for a controlled\n   * layout; omit it and use `defaultLayout` for the uncontrolled form.\n   */\n  layout?: RibbonLayout | (string & {});\n  /** Initial layout (uncontrolled). Default `\"single-line\"`. Unknown values degrade to single-line. */\n  defaultLayout?: RibbonLayout | (string & {});\n  /** Called when the layout changes (e.g. via `RibbonLayoutSwitcher`). */\n  onLayoutChange?: (layout: RibbonLayout) => void;\n  /** Selected tab value (controlled). */\n  value?: string;\n  /** Initially-selected tab value (uncontrolled). */\n  defaultValue?: string;\n  /** Tabs-only \"Mostrar apenas as guias\" mode (controlled). */\n  collapsed?: boolean;\n  /** Initial tabs-only state (uncontrolled). */\n  defaultCollapsed?: boolean;\n  /** Called when the collapsed state changes. */\n  onCollapsedChange?: (collapsed: boolean) => void;\n  /**\n   * **classic only** (Word's \"Ajustar automaticamente\") — **controlled**. `true` =\n   * staged group collapse then scroll; `false` = no collapse, straight to the\n   * scroll UI when the band overflows. Accepted but ignored in single-line. Pair\n   * with `onAutoAdjustChange`, or use `defaultAutoAdjust` for the uncontrolled form.\n   */\n  autoAdjust?: boolean;\n  /** Initial classic adaptive-resize state (uncontrolled). Default `true`. */\n  defaultAutoAdjust?: boolean;\n  /** Called when the `autoAdjust` axis changes (e.g. via `RibbonLayoutSwitcher`). */\n  onAutoAdjustChange?: (autoAdjust: boolean) => void;\n}\n\n/**\n * Ribbon (root) — wraps kit `Tabs` and owns the two state axes (selected tab +\n * collapsed). Provides `RibbonContext`. Stamps `data-slot=\"ribbon\"`,\n * `data-layout`, and `data-collapsed`.\n */\nfunction Ribbon({\n  className,\n  layout,\n  defaultLayout = \"single-line\",\n  onLayoutChange,\n  value,\n  defaultValue,\n  onValueChange,\n  collapsed,\n  defaultCollapsed = false,\n  onCollapsedChange,\n  autoAdjust,\n  defaultAutoAdjust = true,\n  onAutoAdjustChange,\n  children,\n  ...props\n}: RibbonProps) {\n  const baseId = useId();\n\n  // Controllable `layout`, mirroring `collapsed`. Unknown values are normalised\n  // to single-line at the boundary (both the controlled prop and the default), so\n  // the resolved layout — and the value passed to `onLayoutChange` — is always a\n  // clean `RibbonLayout`, and a forward-compat caller never breaks.\n  const [resolvedLayout, setLayout] = useControllableState<RibbonLayout>(\n    layout === undefined ? undefined : normalizeLayout(layout),\n    normalizeLayout(defaultLayout),\n    onLayoutChange\n  );\n\n  const [collapsedState, setCollapsedState] = useControllableState(\n    collapsed,\n    defaultCollapsed,\n    onCollapsedChange\n  );\n\n  // Controllable `autoAdjust`, same pattern — so `RibbonLayoutSwitcher` can drive\n  // it through the context while a controlled caller keeps ownership.\n  const [autoAdjustState, setAutoAdjust] = useControllableState(\n    autoAdjust,\n    defaultAutoAdjust,\n    onAutoAdjustChange\n  );\n\n  // Own the selected tab value (controllable) so the tab-strip overflow menu can\n  // activate a folded tab and `RibbonTab` can pin the active one. External\n  // `onValueChange` still fires with Base UI's original args (value, event).\n  const [activeValue, setActiveValueInternal] = useControllableState<\n    string | undefined\n  >(value, defaultValue);\n  const handleValueChange = useCallback(\n    (next: string, ...rest: unknown[]) => {\n      setActiveValueInternal(next);\n      (onValueChange as ((v: string, ...r: unknown[]) => void) | undefined)?.(\n        next,\n        ...rest\n      );\n    },\n    [setActiveValueInternal, onValueChange]\n  );\n\n  const tabId = useCallback(\n    (v: unknown) => `${baseId}-tab-${String(v)}`,\n    [baseId]\n  );\n\n  const onTabActivate = useCallback(() => {\n    // Activating any tab exits tabs-only mode (Word behavior).\n    setCollapsedState(false);\n  }, [setCollapsedState]);\n\n  const selectValue = useCallback(\n    (next: string) => {\n      handleValueChange(next);\n      setCollapsedState(false);\n    },\n    [handleValueChange, setCollapsedState]\n  );\n\n  const ctx = useMemo<RibbonContextValue>(\n    () => ({\n      layout: resolvedLayout,\n      collapsed: collapsedState,\n      autoAdjust: autoAdjustState,\n      activeValue,\n      selectValue,\n      onTabActivate,\n      tabId,\n      setLayout,\n      setCollapsed: setCollapsedState,\n      setAutoAdjust,\n    }),\n    [\n      resolvedLayout,\n      collapsedState,\n      autoAdjustState,\n      activeValue,\n      selectValue,\n      onTabActivate,\n      tabId,\n      setLayout,\n      setCollapsedState,\n      setAutoAdjust,\n    ]\n  );\n\n  return (\n    <RibbonContext.Provider value={ctx}>\n      <Tabs\n        data-slot=\"ribbon\"\n        data-layout={resolvedLayout}\n        data-collapsed={collapsedState ? \"\" : undefined}\n        value={activeValue}\n        onValueChange={handleValueChange}\n        className={cn(\"gap-0\", className)}\n        {...props}\n      >\n        {children}\n      </Tabs>\n    </RibbonContext.Provider>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/* RibbonTabList / RibbonTab                                                    */\n/* -------------------------------------------------------------------------- */\n\nexport interface RibbonTabListProps extends ComponentProps<typeof TabsList> {\n  /**\n   * **Extra** slack subtracted from the measured strip width, on top of the\n   * `Overflow` manager's accurate accounting (tabs + flex gaps + the `⌄` trigger\n   * are measured). Default `0`.\n   */\n  padding?: number;\n  /**\n   * Injectable measurement fn forwarded to the tab-strip `Overflow`. Default\n   * reads `offsetWidth`. Escape hatch for tests/SSR — you rarely set this.\n   */\n  getSize?: ComponentProps<typeof Overflow>[\"getSize\"];\n  /**\n   * Content pinned to the **far right** of the tab strip — Word's home for the\n   * `RibbonLayoutSwitcher` chevron (validation finding #10). Rendered as a sibling\n   * *outside* both the `role=\"tablist\"` (so a non-tab control never trips axe's\n   * `aria-required-children`) **and** the tab-strip `Overflow` budget (so the\n   * switcher never folds and the trailing tabs overflow *before* reaching it, just\n   * like Word). Pass `<RibbonLayoutSwitcher />` here, or any consumer affordance.\n   */\n  actions?: ReactNode;\n}\n\n/**\n * RibbonTabList — the guide strip. Composes kit `TabsList` (the Fluent sliding\n * underline is Figma-validated there) and wires the v1 `Overflow` machinery so\n * that when the strip is too narrow the **trailing** tabs fold behind a `⌄`\n * chevron menu (Word's tab-strip overflow, captured live at ~680px). The active\n * tab is pinned (see `RibbonTab`) so it can never fold. Stamps\n * `data-slot=\"ribbon-tab-list\"`.\n *\n * ### Far-right `actions` (the layout switcher's home)\n * The optional `actions` slot renders far-right, *outside* the `Overflow` viewport\n * so it is never part of the tab-strip budget — the tabs overflow into the `⌄`\n * menu before they'd ever collide with it (Word's pinned switcher position). The\n * `Overflow` viewport is `flex-1 min-w-0`, so `actions` genuinely reserves its\n * width from the strip. Placement decision (see the file doc comment / the\n * `RibbonLayoutSwitcher` note): the switcher is composable either here or\n * standalone; here mirrors Word.\n */\nfunction RibbonTabList({\n  className,\n  children,\n  padding = 0,\n  getSize,\n  actions,\n  ...props\n}: RibbonTabListProps) {\n  const [registry] = useState(() => createRibbonRegistry());\n  const tabListCtx = useMemo<RibbonTabListContextValue>(\n    () => ({ registry }),\n    [registry]\n  );\n  return (\n    <RibbonTabListContext.Provider value={tabListCtx}>\n      <div data-slot=\"ribbon-tab-list-row\" className=\"flex w-full items-center\">\n        {/* The Overflow container is a flex viewport wrapping the TabsList and the\n            `⌄` trigger. The trigger is a SIBLING of the tablist, never a child of\n            it: a non-tab button inside `role=\"tablist\"` fails axe's\n            `aria-required-children` (tablists may contain only tabs). The tabs\n            stay inside TabsList, so its roving tabindex and sliding indicator are\n            untouched. `flex-1 min-w-0` leaves room for the far-right `actions`. */}\n        <Overflow padding={padding} getSize={getSize}>\n          <div\n            data-slot=\"ribbon-tab-list-viewport\"\n            className=\"relative flex min-w-0 flex-1 items-center overflow-hidden\"\n          >\n            <TabsList\n              data-slot=\"ribbon-tab-list\"\n              className={cn(\"flex-nowrap\", className)}\n              {...props}\n            >\n              {children}\n            </TabsList>\n            <RibbonTabOverflowMenu />\n          </div>\n        </Overflow>\n        {actions ? (\n          <div\n            data-slot=\"ribbon-tab-list-actions\"\n            className=\"flex shrink-0 items-center\"\n          >\n            {actions}\n          </div>\n        ) : null}\n      </div>\n    </RibbonTabListContext.Provider>\n  );\n}\n\nexport interface RibbonTabProps extends ComponentProps<typeof TabsTrigger> {\n  value: string;\n  /**\n   * Menu-form text for the tab-strip overflow menu when this tab folds. Defaults\n   * to `children` when it is a string, else `value`.\n   */\n  label?: string;\n  /** Icon shown beside the label in the tab-strip overflow menu. */\n  icon?: ReactNode;\n}\n\n/**\n * RibbonTab — one guide. Composes kit `TabsTrigger` and stamps the deterministic\n * `id` its command row is labelled by (`aria-labelledby`). Clicking it (mouse or\n * keyboard) un-collapses a tabs-only ribbon, even when it is already the active\n * tab (Word behavior) — hence the un-collapse rides `onClick`, not just tab\n * selection.\n *\n * For tab-strip overflow it is wrapped in an `OverflowItem` (the child is cloned\n * — no wrapper element — so `TabsList`'s roving tabindex and the sliding\n * indicator keep working) and registered in the tab-list registry so the `⌄`\n * menu can offer it while it is folded. It is **pinned while it is the active\n * tab** (`pinned={value === activeValue}`) so the active tab can never fold; the\n * pinned flip re-registers with the `Overflow` manager, which re-ranks, so a just\n * -activated (previously folded) tab immediately becomes visible.\n *\n * `data-slot=\"ribbon-tab\"` is applied **after** the spread so it survives the\n * `OverflowItem` clone (which would otherwise stamp `overflow-item`).\n */\nfunction RibbonTab({\n  className,\n  value,\n  id,\n  label,\n  icon,\n  children,\n  onClick,\n  ...props\n}: RibbonTabProps) {\n  const { tabId, onTabActivate, activeValue } = useRibbonContext();\n  const tabListCtx = useContext(RibbonTabListContext);\n  const menuLabel =\n    label ?? (typeof children === \"string\" ? children : String(value));\n\n  // Publish to the tab-list registry so the overflow menu can render this tab\n  // while it is folded (mutates in place for an existing id — no snapshot churn).\n  useLayoutEffectSafe(() => {\n    tabListCtx?.registry.register({ id: value, label: menuLabel, icon });\n  });\n  useLayoutEffectSafe(() => {\n    return () => tabListCtx?.registry.unregister(value);\n  }, [tabListCtx, value]);\n\n  const trigger = (\n    <RibbonTabTrigger\n      id={id ?? tabId(value)}\n      value={value}\n      onClick={(event) => {\n        onTabActivate();\n        onClick?.(event);\n      }}\n      // h-9 (36px): Word's ribbon guide strip is compact (~36px), unlike the\n      // kit Tabs default 44px (Figma-validated for standalone use) — see\n      // docs/design/ribbon-validation.md finding #1.\n      className={cn(\"h-9\", className)}\n      {...props}\n    >\n      {children}\n    </RibbonTabTrigger>\n  );\n\n  // Outside a tab-list registry (defensive) there is no overflow wiring.\n  if (!tabListCtx) return trigger;\n\n  return (\n    <OverflowItem id={value} pinned={value === activeValue}>\n      {trigger}\n    </OverflowItem>\n  );\n}\n\n/**\n * Inner tab trigger. `OverflowItem` clones its child and injects\n * `data-slot=\"overflow-item\"`; this wrapper re-applies `data-slot=\"ribbon-tab\"`\n * **after** the spread so the tab keeps its public slot (the overflow wiring —\n * ref/style/`aria-hidden`/`data-overflowing` — still flows through the spread).\n */\nfunction RibbonTabTrigger(props: ComponentProps<typeof TabsTrigger>) {\n  return <TabsTrigger {...props} data-slot=\"ribbon-tab\" />;\n}\n\nfunction useRibbonTabListContext(): RibbonTabListContextValue {\n  const ctx = useContext(RibbonTabListContext);\n  if (!ctx) {\n    throw new Error(\"RibbonTab overflow parts must be inside a <RibbonTabList>.\");\n  }\n  return ctx;\n}\n\n/**\n * RibbonTabOverflowMenu — the `⌄` chevron + menu for folded tabs. Rendered\n * automatically by `RibbonTabList` (you don't place it), as the last child of the\n * `TabsList` so it stays inside the tab-strip `Overflow` context and its width is\n * reserved from the budget (`useOverflowMenu`). It is kept measurable-but-hidden\n * (out of flow + `visibility:hidden`) until a tab folds, then shows at the strip's\n * end. The menu lists only the **hidden** tabs (source order); choosing one\n * activates it via the ribbon context (which also un-collapses a tabs-only\n * ribbon). It is a real keyboard-reachable button (the only route to folded tabs).\n */\nfunction RibbonTabOverflowMenu() {\n  const { registry } = useRibbonTabListContext();\n  const { ref, isOverflowing, overflowCount } =\n    useOverflowMenu<HTMLButtonElement>();\n\n  const tabs = useSyncExternalStore(\n    registry.subscribe,\n    registry.getSnapshot,\n    registry.getServerSnapshot\n  );\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        render={\n          <button\n            ref={ref}\n            type=\"button\"\n            data-slot=\"ribbon-tab-overflow-trigger\"\n            data-overflow-menu=\"\"\n            aria-label={\n              overflowCount === 1 ? \"1 guia oculta\" : `${overflowCount} guias ocultas`\n            }\n            title=\"Mais guias\"\n            className={cn(\n              \"flex h-9 shrink-0 items-center justify-center rounded-md px-2 text-muted-foreground\",\n              \"cursor-pointer outline-none transition-colors duration-fast ease-ease\",\n              \"hover:bg-accent hover:text-foreground active:bg-accent/80 dark:hover:bg-accent/50\",\n              \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n              isOverflowing ? \"ml-auto\" : undefined\n            )}\n            style={isOverflowing ? undefined : HIDDEN_TRIGGER_STYLE}\n          >\n            <ChevronGlyph className=\"size-4\" />\n          </button>\n        }\n      />\n      <DropdownMenuContent\n        align=\"end\"\n        className=\"min-w-40\"\n        data-slot=\"ribbon-tab-overflow-menu\"\n      >\n        {tabs.map((tab) => (\n          <RibbonHiddenTabRow key={tab.id} meta={tab} />\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\n/** One folded tab, rendered only while it is hidden from the strip. */\nfunction RibbonHiddenTabRow({ meta }: { meta: RibbonItemMeta }) {\n  const { selectValue } = useRibbonContext();\n  const visible = useIsOverflowItemVisible(meta.id);\n  if (visible) return null;\n  return (\n    <DropdownMenuItem onClick={() => selectValue(meta.id)}>\n      {meta.icon}\n      {meta.label}\n    </DropdownMenuItem>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/* RibbonContent (the command row)                                             */\n/* -------------------------------------------------------------------------- */\n\nexport interface RibbonContentProps\n  extends Omit<ComponentProps<typeof TabsContent>, \"value\"> {\n  value: string;\n  /**\n   * **Extra** slack subtracted from the measured row width, on top of the\n   * `Overflow` manager's accurate accounting (item widths + group dividers +\n   * flex gaps + the \"…\" trigger are all measured now — you no longer hand-tune\n   * this to cover them). Default `0`.\n   */\n  padding?: number;\n  /** Floor on non-pinned items kept in the bar (they clip rather than vanish). Default `1`. */\n  minimumVisible?: number;\n  /**\n   * Injectable measurement fn. **single-line:** forwarded to `Overflow`;\n   * **classic:** forwarded to `GroupCollapse` (its `axis` second arg is ignored\n   * by an `(el) => number` fn). Default reads `offsetWidth`. Escape hatch for\n   * tests/SSR where layout is unavailable — you almost never set this in app code.\n   */\n  getSize?: ComponentProps<typeof Overflow>[\"getSize\"];\n  /**\n   * **classic only.** Optimistic width used for a group's collapsed dropdown\n   * button *before* it has been measured (a `display:none` form reports 0).\n   * Kept low so the collapse loop only under-collapses and the C1 settle net\n   * corrects (see `ribbon-collapse.tsx`). Default `64`.\n   */\n  collapsedEstimate?: number;\n  /** **classic only.** Injectable flex-gap reader forwarded to `GroupCollapse`. */\n  getGap?: CollapseGetGap;\n  /** **classic only.** Injectable overflow-extent reader for the C1 settle net. */\n  getOverflowSize?: CollapseGetOverflowSize;\n}\n\n/**\n * RibbonContent — one tab's command band.\n *\n * **single-line:** a kit `Toolbar` wrapped in `Overflow`, wired end-to-end, with\n * the `RibbonOverflowMenu` appended after your groups (~40px tall, thin group\n * dividers).\n *\n * **classic:** the SAME `Toolbar` wrapped in a `GroupCollapse` provider (the C1\n * group-collapse manager) — a ~96px two-row band, no \"…\" menu (Word classic has\n * none; the terminal fallback is scroll, which `GroupCollapse` flags by stamping\n * `data-scroll-mode` on the band — the scroll UI itself is C3). Both layouts take\n * the toolbar's accessible name from the owning tab (`aria-labelledby` → tab id).\n *\n * When the ribbon is `collapsed` (tabs-only), the band is hidden (via the\n * `hidden` attribute, not unmounted — the `keepMounted` note below).\n */\nfunction RibbonContent({\n  className,\n  value,\n  padding = 0,\n  minimumVisible = 1,\n  getSize,\n  collapsedEstimate = 64,\n  getGap,\n  getOverflowSize,\n  children,\n  ...props\n}: RibbonContentProps) {\n  const { tabId, collapsed, layout, autoAdjust } = useRibbonContext();\n  const isClassic = layout === \"classic\";\n  const [registry] = useState(() => createRibbonRegistry());\n  const overflowTriggerRef = useRef<HTMLElement | null>(null);\n\n  const contentCtx = useMemo<RibbonContentContextValue>(\n    () => ({ registry, overflowTriggerRef }),\n    [registry]\n  );\n\n  // Mark the last RibbonGroup so it drops its trailing divider (no dangling\n  // hairline before the \"…\"). Only injected when the consumer didn't set it.\n  const preparedChildren = useMemo(() => {\n    const array = Children.toArray(children);\n    let lastGroupIndex = -1;\n    array.forEach((child, index) => {\n      if (isValidElement(child) && child.type === RibbonGroup) {\n        lastGroupIndex = index;\n      }\n    });\n    if (lastGroupIndex === -1) return children;\n    return array.map((child, index) => {\n      if (\n        index === lastGroupIndex &&\n        isValidElement(child) &&\n        (child.props as RibbonGroupProps).withTrailingDivider === undefined\n      ) {\n        return cloneElement(child as ReactElement<RibbonGroupProps>, {\n          withTrailingDivider: false,\n        });\n      }\n      return child;\n    });\n  }, [children]);\n\n  return (\n    <TabsContent\n      data-slot=\"ribbon-content\"\n      value={value}\n      // Keep every panel mounted so the `Overflow` manager is created exactly\n      // once (at first render) and never unmounted by a tab switch. The overflow\n      // manager tears itself down on unmount; remounting it under React\n      // StrictMode's dev double-invoke would leave it destroyed (its items would\n      // read an empty snapshot and all hide). Keeping panels mounted — and\n      // hiding the row with CSS when `collapsed` rather than unmounting — sidesteps\n      // that entirely and is also cheaper (no re-measure on every tab switch).\n      keepMounted\n      data-collapsed={collapsed ? \"\" : undefined}\n      className={cn(\"pt-0 outline-none\", className)}\n      {...props}\n    >\n      <RibbonContentContext.Provider value={contentCtx}>\n        {/*\n          When collapsed, hide the band with the `hidden` attribute (real\n          display:none, and — unlike a CSS class — honoured by jsdom / the\n          accessibility tree) instead of unmounting, so the `Overflow` /\n          `GroupCollapse` manager survives (see the `keepMounted` note above).\n        */}\n        <div hidden={collapsed || undefined}>\n          {isClassic ? (\n            <ClassicBand\n              value={value}\n              tabId={tabId}\n              autoAdjust={autoAdjust}\n              collapsedEstimate={collapsedEstimate}\n              padding={padding}\n              getSize={getSize as CollapseGetSize | undefined}\n              getGap={getGap}\n              getOverflowSize={getOverflowSize}\n            >\n              {preparedChildren}\n            </ClassicBand>\n          ) : (\n            <Overflow\n              padding={padding}\n              minimumVisible={minimumVisible}\n              getSize={getSize}\n            >\n              <Toolbar\n                aria-labelledby={tabId(value)}\n                className={cn(\n                  // ~40px transparent band with a chrome hairline; clip residuals.\n                  \"relative min-h-10 w-full flex-nowrap items-center overflow-hidden\",\n                  \"border-b border-border bg-background px-1\"\n                )}\n              >\n                {preparedChildren}\n                <RibbonOverflowMenu />\n              </Toolbar>\n            </Overflow>\n          )}\n        </div>\n      </RibbonContentContext.Provider>\n    </TabsContent>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/* Classic band + horizontal-scroll fallback (C3)                              */\n/* -------------------------------------------------------------------------- */\n\ninterface ClassicBandProps {\n  value: string;\n  tabId: (value: unknown) => string;\n  autoAdjust: boolean;\n  collapsedEstimate: number;\n  padding: number;\n  getSize?: CollapseGetSize;\n  getGap?: CollapseGetGap;\n  getOverflowSize?: CollapseGetOverflowSize;\n  children: ReactNode;\n}\n\n/**\n * ClassicBand — the classic command band plus its C3 horizontal-scroll fallback.\n *\n * The `Toolbar` is always a real (clipped) horizontal scroll container. Two paths:\n * - **`autoAdjust` (default):** the band is wrapped in the `GroupCollapse`\n *   provider (staged group collapse). A zero-DOM `ScrollModeReporter` inside the\n *   band lifts `useIsScrollMode()` up here, so the edge arrows only arm once every\n *   group has collapsed and the band *still* overflows (the terminal scroll\n *   fallback) — no arrow flicker during collapse.\n * - **`autoAdjust={false}`:** no `GroupCollapse` at all — every group stays\n *   expanded (see `RibbonGroupClassicStatic`) and the arrows are always armed, so\n *   the band goes straight to scroll the moment it overflows.\n *\n * The `‹`/`›` buttons are absolute siblings of the `Toolbar` (outside it, so Base\n * UI's roving tabindex never owns them) inside a `relative` wrapper; each shows\n * only when there is clipped content on its side.\n */\nfunction ClassicBand({\n  value,\n  tabId,\n  autoAdjust,\n  collapsedEstimate,\n  padding,\n  getSize,\n  getGap,\n  getOverflowSize,\n  children,\n}: ClassicBandProps) {\n  const bandRef = useRef<HTMLElement | null>(null);\n  const [scrollMode, setScrollMode] = useState(false);\n\n  // Non-adaptive: no collapse, so arm the arrows whenever content clips.\n  // Adaptive: arm only in the terminal scroll fallback (all groups collapsed).\n  const armed = autoAdjust ? scrollMode : true;\n  const { left, right } = useScrollAffordance(bandRef, armed);\n\n  const toolbar = (\n    <Toolbar\n      ref={bandRef as Ref<HTMLDivElement>}\n      aria-labelledby={tabId(value)}\n      className={cn(\n        // ~96px two-row classic band: content area + group labels row.\n        // `items-stretch` so each group form fills the band height and can pin\n        // its label to the bottom; `overflow-hidden` clips residuals so the C1\n        // accounting + settle net see a real overrun AND makes the band a\n        // programmatic (scrollbar-less) horizontal scroll container — Word's model.\n        \"relative h-24 w-full flex-nowrap items-stretch overflow-hidden\",\n        \"border-b border-border bg-background px-1\"\n      )}\n    >\n      {children}\n      {autoAdjust ? <ScrollModeReporter onChange={setScrollMode} /> : null}\n    </Toolbar>\n  );\n\n  return (\n    <div data-slot=\"ribbon-classic-band\" className=\"relative\">\n      {autoAdjust ? (\n        <GroupCollapse\n          collapsedEstimate={collapsedEstimate}\n          padding={padding}\n          getSize={getSize}\n          getGap={getGap}\n          getOverflowSize={getOverflowSize}\n        >\n          {toolbar}\n        </GroupCollapse>\n      ) : (\n        toolbar\n      )}\n      {left ? (\n        <ClassicScrollButton\n          direction=\"left\"\n          onClick={() => scrollBandBy(bandRef.current, \"left\")}\n        />\n      ) : null}\n      {right ? (\n        <ClassicScrollButton\n          direction=\"right\"\n          onClick={() => scrollBandBy(bandRef.current, \"right\")}\n        />\n      ) : null}\n    </div>\n  );\n}\n\n/**\n * ScrollModeReporter — a zero-DOM child that lives inside the `GroupCollapse`\n * context, reads `useIsScrollMode()`, and lifts it to `ClassicBand`. Rendering it\n * inside the band (rather than reading scroll mode outside `GroupCollapse`, where\n * the context is unavailable) keeps the arrows fully driven by the C1 snapshot\n * while staying siblings of the `Toolbar`. Returns `null`, so it neither joins the\n * roving order nor contributes to measurement.\n */\nfunction ScrollModeReporter({\n  onChange,\n}: {\n  onChange: (scrollMode: boolean) => void;\n}) {\n  const scrollMode = useIsScrollMode();\n  useEffect(() => {\n    onChange(scrollMode);\n  }, [onChange, scrollMode]);\n  return null;\n}\n\n/** Does the user prefer reduced motion? (SSR-safe.) */\nfunction prefersReducedMotion(): boolean {\n  return (\n    typeof window !== \"undefined\" &&\n    typeof window.matchMedia === \"function\" &&\n    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  );\n}\n\n/** Scroll a band by ~half its visible width toward `direction` (motion-aware). */\nfunction scrollBandBy(el: HTMLElement | null, direction: \"left\" | \"right\") {\n  if (!el) return;\n  const amount = Math.max(1, Math.floor(el.clientWidth / 2));\n  el.scrollBy({\n    left: direction === \"left\" ? -amount : amount,\n    behavior: prefersReducedMotion() ? \"auto\" : \"smooth\",\n  });\n}\n\n/**\n * useScrollAffordance — tracks whether the band has clipped content on each edge.\n * `left` when `scrollLeft > 0`; `right` when there is more scrollable extent past\n * the right edge (with a 1px tolerance). Re-measures on scroll, on container\n * resize (`ResizeObserver`), and whenever `enabled` flips. State only changes when\n * a boolean actually flips, so it settles without loops (showing/hiding an\n * absolute arrow never resizes the observed band).\n */\nfunction useScrollAffordance(\n  ref: { current: HTMLElement | null },\n  enabled: boolean\n): { left: boolean; right: boolean } {\n  const [state, setState] = useState({ left: false, right: false });\n\n  const measure = useCallback(() => {\n    const el = ref.current;\n    if (!el || !enabled) {\n      setState((prev) =>\n        prev.left || prev.right ? { left: false, right: false } : prev\n      );\n      return;\n    }\n    const left = el.scrollLeft > 0;\n    const right = el.scrollWidth - el.clientWidth - el.scrollLeft > 1;\n    setState((prev) =>\n      prev.left === left && prev.right === right ? prev : { left, right }\n    );\n  }, [ref, enabled]);\n\n  useLayoutEffectSafe(() => {\n    measure();\n    const el = ref.current;\n    if (!el) return;\n    el.addEventListener(\"scroll\", measure, { passive: true });\n    let observer: ResizeObserver | undefined;\n    if (typeof ResizeObserver !== \"undefined\") {\n      observer = new ResizeObserver(() => measure());\n      observer.observe(el);\n    }\n    return () => {\n      el.removeEventListener(\"scroll\", measure);\n      observer?.disconnect();\n    };\n  }, [measure]);\n\n  return state;\n}\n\n/** Left/right chevron glyph for the scroll arrows (inline SVG, breadcrumb style). */\nfunction ScrollChevron({ direction }: { direction: \"left\" | \"right\" }) {\n  return (\n    <svg viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\" className=\"size-4\">\n      <path\n        d={direction === \"left\" ? \"M12 5l-5 5 5 5\" : \"M8 5l5 5-5 5\"}\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\n/**\n * ClassicScrollButton — one edge arrow. A real `<button>` with an `aria-label`\n * (NOT `aria-hidden` — it is an interactive control), but taken out of the tab\n * order (`tabIndex={-1}`): it is a **pointer affordance**, since keyboard users\n * reach clipped commands through the toolbar's roving focus, which scrolls the\n * focused item into view natively. Absolute-positioned over the band edge with a\n * token-driven gradient fade. Stamps `data-slot=\"ribbon-scroll-button\"`.\n */\nfunction ClassicScrollButton({\n  direction,\n  onClick,\n}: {\n  direction: \"left\" | \"right\";\n  onClick: () => void;\n}) {\n  const isLeft = direction === \"left\";\n  return (\n    <button\n      type=\"button\"\n      tabIndex={-1}\n      data-slot=\"ribbon-scroll-button\"\n      data-direction={direction}\n      aria-label={\n        isLeft\n          ? \"Rolar comandos para a esquerda\"\n          : \"Rolar comandos para a direita\"\n      }\n      onClick={onClick}\n      className={cn(\n        \"absolute inset-y-px z-10 flex w-11 items-center from-background from-45% to-transparent outline-none\",\n        isLeft\n          ? \"left-0 justify-start bg-gradient-to-r pl-0.5\"\n          : \"right-0 justify-end bg-gradient-to-l pr-0.5\"\n      )}\n    >\n      <span\n        className={cn(\n          \"flex size-7 items-center justify-center rounded-md border border-border bg-background text-muted-foreground shadow-8\",\n          \"transition-colors duration-fast ease-ease hover:bg-accent hover:text-foreground active:bg-accent/80 dark:hover:bg-accent/50\"\n        )}\n      >\n        <ScrollChevron direction={direction} />\n      </span>\n    </button>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/* RibbonGroup                                                                  */\n/* -------------------------------------------------------------------------- */\n\nexport interface RibbonGroupProps extends ComponentProps<\"div\"> {\n  /** Stable group id; inherited by the group's items and used for its divider. */\n  groupId: string;\n  /** Human label — the overflow section header (single-line) / the centered muted band label + collapsed-button name (classic). */\n  label: string;\n  /** Render the trailing divider (auto-suppressed on the last group). Default `true`. */\n  withTrailingDivider?: boolean;\n  /**\n   * **classic only.** Higher collapses FIRST as the band shrinks (Word:\n   * Parágrafo before Fonte). Governs classic collapse order; **ignored in\n   * single-line** (there `RibbonItem.priority` rules). Default `0`.\n   */\n  collapsePriority?: number;\n  /** **classic only.** Icon shown on the group's collapsed dropdown button. */\n  icon?: ReactNode;\n  /**\n   * **classic only.** Dialog-launcher (↘) slot in the group's bottom-right. Pass\n   * a custom node here (full control), OR pass `onLauncherClick` for the default\n   * ↘ icon-button. `launcher` wins if both are set. Omit both for no launcher.\n   */\n  launcher?: ReactNode;\n  /** **classic only.** Sugar for the default ↘ launcher button (see `launcher`). */\n  onLauncherClick?: () => void;\n  /**\n   * Render this group only in the listed layouts (Word shows different command\n   * sets per mode). When set and the active layout isn't listed, the group\n   * renders nothing. Default: both layouts.\n   */\n  layouts?: RibbonLayout[];\n}\n\n/**\n * RibbonGroup — a logical command cluster. Adapts to the root `layout`:\n * - **single-line:** a context provider (no DOM box) that flattens its items +\n *   an `OverflowDivider`-wrapped `RibbonSeparator` into the toolbar row.\n * - **classic:** a `CollapseGroup` (C1) rendering the group's two forms — the\n *   expanded band anatomy and the collapsed dropdown button.\n *\n * The `layouts` escape hatch renders nothing (and, in single-line, registers\n * nothing) when the active layout isn't listed.\n */\nfunction RibbonGroup(props: RibbonGroupProps) {\n  const { layout, autoAdjust } = useRibbonContext();\n  if (props.layouts && !props.layouts.includes(layout)) return null;\n  if (layout === \"classic\") {\n    // `autoAdjust={false}` → no GroupCollapse provider is mounted, so the group\n    // must NOT call `useGroupMode`/render a `CollapseGroup` (both need that\n    // context). It renders its expanded form only; the band goes straight to\n    // scroll when it overflows.\n    return autoAdjust ? (\n      <RibbonGroupClassic {...props} />\n    ) : (\n      <RibbonGroupClassicStatic {...props} />\n    );\n  }\n  return <RibbonGroupSingleLine {...props} />;\n}\n\n/**\n * RibbonGroup (single-line) — the v1 behavior, unchanged. Provides\n * `groupId` + `label` to its `RibbonItem`s via context (so each item inherits\n * its group + section header), renders them, then an `OverflowDivider`-wrapped\n * `RibbonSeparator`. The divider hides itself only when the whole group is\n * overflowed (`useIsOverflowGroupVisible` → `\"hidden\"`), so no separator dangles.\n * Renders no DOM element of its own.\n */\nfunction RibbonGroupSingleLine({\n  groupId,\n  label,\n  withTrailingDivider = true,\n  children,\n}: RibbonGroupProps) {\n  const ctx = useMemo<RibbonGroupContextValue>(\n    () => ({ groupId, groupLabel: label }),\n    [groupId, label]\n  );\n  return (\n    <RibbonGroupContext.Provider value={ctx}>\n      {children}\n      {withTrailingDivider ? (\n        <OverflowDivider groupId={groupId}>\n          <RibbonSeparator />\n        </OverflowDivider>\n      ) : null}\n    </RibbonGroupContext.Provider>\n  );\n}\n\n/** Down-chevron glyph — inline SVG (no icon dependency), à la split-button. */\nfunction ChevronGlyph({ className = \"size-3\" }: { className?: string }) {\n  return (\n    <svg viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\" className={className}>\n      <path\n        d=\"M6 8l4 4 4-4\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\n/** Dialog-launcher glyph (↘) — the Fluent \"open dialog\" corner arrow. */\nfunction DialogLauncherGlyph() {\n  return (\n    <svg viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5\">\n      <path\n        d=\"M13 5h2v2M15 5l-4 4M6 6v8h8\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\n/**\n * RibbonGroup (classic) — the crux. Renders a `CollapseGroup` (C1) with two form\n * shells, both kept mounted for measurement:\n *\n * - **expanded:** a `role=\"group\"` div — the horizontal cluster of children over\n *   a centered muted `label`, an optional ↘ launcher bottom-right, and a trailing\n *   hairline (suppressed on the last group). Children render here **only while\n *   expanded**.\n * - **collapsed:** a fixed-size icon + label + chevron `ToolbarButton` (a kit\n *   `Popover` trigger, so it joins the toolbar roving-tabindex) whose flyout holds\n *   the group's SAME children — rendered there **only while collapsed** (and the\n *   flyout is open). The shell's size is independent of the children.\n *\n * **Focus-on-collapse:** if the focused control's group flips to collapsed, focus\n * moves to that group's collapsed dropdown button — the ribbon's focus-to-trigger\n * pattern at group level (snapshot focus during the flipping render, move it in\n * the post-commit effect).\n */\nfunction RibbonGroupClassic({\n  groupId,\n  label,\n  icon,\n  collapsePriority = 0,\n  launcher,\n  onLauncherClick,\n  withTrailingDivider = true,\n  className,\n  children,\n}: RibbonGroupProps) {\n  const groupCtx = useMemo<RibbonGroupContextValue>(\n    () => ({ groupId, groupLabel: label }),\n    [groupId, label]\n  );\n  const mode = useGroupMode(groupId);\n  const isCollapsed = mode === \"collapsed\";\n  const [open, setOpen] = useState(false);\n\n  const expandedShellRef = useRef<HTMLDivElement | null>(null);\n  const collapsedButtonRef = useRef<HTMLButtonElement | null>(null);\n  const heldFocusBeforeCollapseRef = useRef(false);\n\n  // Snapshot focus during the render that flips this group collapsed — the DOM\n  // is still pre-commit here (the expanded shell is visible and holds focus), so\n  // we capture \"did a control in me hold focus?\" before C1's `display:none` lands.\n  if (\n    isCollapsed &&\n    typeof document !== \"undefined\" &&\n    expandedShellRef.current &&\n    expandedShellRef.current.contains(document.activeElement)\n  ) {\n    heldFocusBeforeCollapseRef.current = true;\n  }\n\n  // After the collapse commit (expanded shell now display:none, collapsed button\n  // visible) move focus to the button so it isn't dropped to <body>.\n  useLayoutEffectSafe(() => {\n    if (isCollapsed && heldFocusBeforeCollapseRef.current) {\n      collapsedButtonRef.current?.focus();\n      heldFocusBeforeCollapseRef.current = false;\n    }\n  }, [isCollapsed]);\n\n  // Un-collapsing while the flyout is open would leave a dangling popover.\n  useEffect(() => {\n    if (!isCollapsed && open) setOpen(false);\n  }, [isCollapsed, open]);\n\n  const showSeparator = withTrailingDivider !== false;\n  const separator = showSeparator ? (\n    <span\n      aria-hidden=\"true\"\n      data-slot=\"ribbon-group-separator\"\n      className=\"my-2 w-px shrink-0 self-stretch bg-border\"\n    />\n  ) : null;\n\n  const launcherNode =\n    launcher ??\n    (onLauncherClick ? (\n      <ToolbarButton\n        size=\"icon-sm\"\n        aria-label={`${label} — mais opções`}\n        data-slot=\"ribbon-launcher\"\n        onClick={onLauncherClick}\n        className=\"size-5 text-muted-foreground\"\n      >\n        <DialogLauncherGlyph />\n      </ToolbarButton>\n    ) : null);\n\n  const expanded = (\n    <div\n      ref={expandedShellRef}\n      role=\"group\"\n      aria-label={label}\n      data-slot=\"ribbon-group\"\n      data-group-id={groupId}\n      className={cn(\"flex shrink-0 items-stretch\", className)}\n    >\n      <div className=\"flex min-w-0 flex-col justify-between px-1 py-1\">\n        <RibbonGroupContext.Provider value={groupCtx}>\n          <div className=\"flex flex-1 items-start justify-center gap-0.5\">\n            {!isCollapsed ? children : null}\n          </div>\n        </RibbonGroupContext.Provider>\n        <div className=\"relative flex items-center justify-center pt-0.5\">\n          <span className=\"px-1 text-[11px] leading-none text-muted-foreground\">\n            {label}\n          </span>\n          {launcherNode ? (\n            <span className=\"absolute right-0 flex items-center\">\n              {launcherNode}\n            </span>\n          ) : null}\n        </div>\n      </div>\n      {separator}\n    </div>\n  );\n\n  const collapsed = (\n    <div\n      data-slot=\"ribbon-group-collapsed\"\n      data-group-id={groupId}\n      className=\"flex shrink-0 items-stretch\"\n    >\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger\n          render={\n            <ToolbarButton\n              ref={collapsedButtonRef}\n              variant=\"ghost\"\n              aria-label={label}\n              data-slot=\"ribbon-group-trigger\"\n              className=\"flex h-auto min-h-full w-[72px] flex-col items-center justify-center gap-1 px-1 py-1\"\n            >\n              <span className=\"flex items-center justify-center [&_svg:not([class*='size-'])]:size-5\">\n                {icon}\n              </span>\n              <span className=\"flex max-w-full items-center gap-0.5 text-[11px] leading-none\">\n                <span className=\"truncate\">{label}</span>\n                <ChevronGlyph />\n              </span>\n            </ToolbarButton>\n          }\n        />\n        <PopoverContent\n          aria-label={label}\n          align=\"start\"\n          side=\"bottom\"\n          className=\"w-auto min-w-56 p-2\"\n        >\n          <RibbonGroupContext.Provider value={groupCtx}>\n            <div className=\"flex items-start gap-0.5\">\n              {isCollapsed ? children : null}\n            </div>\n          </RibbonGroupContext.Provider>\n        </PopoverContent>\n      </Popover>\n      {separator}\n    </div>\n  );\n\n  return (\n    <CollapseGroup\n      groupId={groupId}\n      collapsePriority={collapsePriority}\n      expanded={expanded}\n      collapsed={collapsed}\n    />\n  );\n}\n\n/**\n * RibbonGroupClassicStatic — the `autoAdjust={false}` classic group: the expanded\n * band anatomy only, always showing its children. It calls **no** collapse hooks\n * and renders no `CollapseGroup` (there is no `GroupCollapse` provider in this\n * mode), so the band never collapses — it simply overflows into the scroll UI.\n * `collapsePriority`/`icon` are irrelevant here (nothing collapses) and ignored.\n */\nfunction RibbonGroupClassicStatic({\n  groupId,\n  label,\n  launcher,\n  onLauncherClick,\n  withTrailingDivider = true,\n  className,\n  children,\n}: RibbonGroupProps) {\n  const groupCtx = useMemo<RibbonGroupContextValue>(\n    () => ({ groupId, groupLabel: label }),\n    [groupId, label]\n  );\n\n  const separator =\n    withTrailingDivider !== false ? (\n      <span\n        aria-hidden=\"true\"\n        data-slot=\"ribbon-group-separator\"\n        className=\"my-2 w-px shrink-0 self-stretch bg-border\"\n      />\n    ) : null;\n\n  const launcherNode =\n    launcher ??\n    (onLauncherClick ? (\n      <ToolbarButton\n        size=\"icon-sm\"\n        aria-label={`${label} — mais opções`}\n        data-slot=\"ribbon-launcher\"\n        onClick={onLauncherClick}\n        className=\"size-5 text-muted-foreground\"\n      >\n        <DialogLauncherGlyph />\n      </ToolbarButton>\n    ) : null);\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={label}\n      data-slot=\"ribbon-group\"\n      data-group-id={groupId}\n      className={cn(\"flex shrink-0 items-stretch\", className)}\n    >\n      <div className=\"flex min-w-0 flex-col justify-between px-1 py-1\">\n        <RibbonGroupContext.Provider value={groupCtx}>\n          <div className=\"flex flex-1 items-start justify-center gap-0.5\">\n            {children}\n          </div>\n        </RibbonGroupContext.Provider>\n        <div className=\"relative flex items-center justify-center pt-0.5\">\n          <span className=\"px-1 text-[11px] leading-none text-muted-foreground\">\n            {label}\n          </span>\n          {launcherNode ? (\n            <span className=\"absolute right-0 flex items-center\">\n              {launcherNode}\n            </span>\n          ) : null}\n        </div>\n      </div>\n      {separator}\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/* RibbonItem (the crux)                                                        */\n/* -------------------------------------------------------------------------- */\n\nexport interface RibbonItemProps {\n  /** Required, unique within the row. */\n  id: string;\n  /** Menu-form text (bar form may be icon-only). */\n  label: string;\n  /** Icon reused in the menu form. */\n  icon?: ReactNode;\n  /**\n   * **single-line only.** Higher survives longer as the row shrinks (priority\n   * beats DOM order). **Ignored in classic** — there the group's\n   * `collapsePriority` rules and the whole group collapses together.\n   */\n  priority?: number;\n  /**\n   * **single-line only.** Never overflows (e.g. Undo, a pinned command).\n   * **Ignored in classic** (nothing item-overflows; groups collapse whole).\n   */\n  pinned?: boolean;\n  /** **single-line only.** Custom menu form (split-button submenu, labeled combo, …). */\n  overflowRender?: () => ReactNode;\n  /** **single-line only.** Fired by the default menu form (a `DropdownMenuItem`). */\n  onSelect?: () => void;\n  /**\n   * Render this item only in the listed layouts (Word shows different command\n   * sets per mode). When set and the active layout isn't listed, the item renders\n   * nothing — and, in single-line, does not register in the overflow menu.\n   * Default: both layouts.\n   */\n  layouts?: RibbonLayout[];\n  /** The bar-form control (ToolbarButton, Toggle, SplitButton, RibbonLargeButton, …). */\n  children: ReactElement;\n}\n\n/**\n * RibbonItem — one command. Adapts to the root `layout`:\n * - **single-line:** wraps `children` in an `OverflowItem` and registers its\n *   menu-form metadata (`RibbonOverflowMenu` renders it when hidden); owns the\n *   focus-preservation move to the \"…\" trigger. `priority`/`pinned`/`onSelect`/\n *   `overflowRender` apply here only.\n * - **classic:** renders `children` directly — no `OverflowItem`, no registry\n *   (\"…\" doesn't exist in classic). Nothing item-overflows; the group collapses\n *   whole (`RibbonGroup.collapsePriority`).\n *\n * The `layouts` escape hatch renders (and, in single-line, registers) nothing\n * when the active layout isn't listed.\n */\nfunction RibbonItem(props: RibbonItemProps) {\n  const { layout } = useRibbonContext();\n  if (props.layouts && !props.layouts.includes(layout)) return null;\n  if (layout === \"classic\") return <>{props.children}</>;\n  return <RibbonItemSingleLine {...props} />;\n}\n\n/** RibbonItem's v1 (single-line) body — the OverflowItem + registry + focus move. */\nfunction RibbonItemSingleLine({\n  id,\n  label,\n  icon,\n  priority,\n  pinned,\n  overflowRender,\n  onSelect,\n  children,\n}: RibbonItemProps) {\n  const { registry, overflowTriggerRef } = useRibbonContentContext();\n  const { groupId, groupLabel } = useContext(RibbonGroupContext);\n  const visible = useIsOverflowItemVisible(id);\n\n  const childElementRef = useRef<HTMLElement | null>(null);\n  const heldFocusBeforeHideRef = useRef(false);\n\n  // Keep the registry entry current every commit (no deps). `register` mutates\n  // in place for an existing id — no new snapshot identity, no re-render loop —\n  // and only rebuilds + notifies on a membership change (first mount).\n  useLayoutEffectSafe(() => {\n    registry.register({\n      id,\n      label,\n      icon,\n      groupId,\n      groupLabel,\n      overflowRender,\n      onSelect,\n    });\n  });\n\n  // Unregister on unmount / id change.\n  useLayoutEffectSafe(() => {\n    return () => registry.unregister(id);\n  }, [registry, id]);\n\n  // Snapshot focus during the render that flips this item hidden — the DOM is\n  // still pre-commit here (item visible, focus still on it), so we can capture\n  // \"did I hold focus?\" before `OverflowItem`'s `display:none` lands this commit.\n  if (\n    !visible &&\n    typeof document !== \"undefined\" &&\n    childElementRef.current &&\n    childElementRef.current.contains(document.activeElement)\n  ) {\n    heldFocusBeforeHideRef.current = true;\n  }\n\n  // A layout effect runs after the display:none commit, when the trigger is\n  // already visible — move focus there so it isn't dropped to <body>.\n  useLayoutEffectSafe(() => {\n    if (!visible && heldFocusBeforeHideRef.current) {\n      overflowTriggerRef.current?.focus();\n      heldFocusBeforeHideRef.current = false;\n    }\n  }, [visible, overflowTriggerRef]);\n\n  const captureChild = useCallback((el: HTMLElement | null) => {\n    childElementRef.current = el;\n  }, []);\n\n  const child = Children.only(children);\n  if (!isValidElement(child)) {\n    throw new Error(\"<RibbonItem> expects a single element child.\");\n  }\n  // Merge our capture ref with the child's own ref. `OverflowItem` merges again\n  // with its measure ref (it reads `child.props.ref`), so the element carries\n  // both. The merged ref is memoised on the child + captureChild so it doesn't\n  // detach/reattach every render.\n  const mergedRef = useMemo(\n    () => mergeRefs<HTMLElement>(getChildRef(child), captureChild),\n    [child, captureChild]\n  );\n  const childWithRef = cloneElement(child, {\n    ref: mergedRef,\n  } as Record<string, unknown>);\n\n  return (\n    <OverflowItem id={id} priority={priority} pinned={pinned} groupId={groupId}>\n      {childWithRef}\n    </OverflowItem>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/* RibbonLayoutSwitcher (Word's far-right display-options chevron)             */\n/* -------------------------------------------------------------------------- */\n\nexport interface RibbonLayoutSwitcherProps {\n  /** Extra classes for the chevron trigger button. */\n  className?: string;\n}\n\n/**\n * RibbonLayoutSwitcher — Word's far-right **display-options** control (validation\n * finding #10): a pinned chevron icon-button opening a menu with two sections that\n * mirror Word's own (pt-BR copy), driven entirely through the ribbon context:\n *\n * - **\"Layout da Faixa de Opções\"** — a radio pair, *Faixa de Opções Clássica* /\n *   *Faixa de Opções de Linha Única*; the active `layout` carries the marker\n *   (`menuitemradio` + `aria-checked`) and selecting one calls `setLayout` (honours\n *   a controlled `layout`).\n * - **\"Mostrar Faixa de Opções\"** — a radio pair on the `collapsed` axis, *Sempre\n *   mostrar faixa de opções* (`collapsed=false`) / *Mostrar apenas as guias*\n *   (`collapsed=true`), plus *Ajustar automaticamente* — a checkbox on the\n *   **classic-only** `autoAdjust` axis, rendered **disabled in single-line** (Word\n *   only exposes it for classic; kept visible-but-disabled so its existence is\n *   discoverable rather than silently missing).\n *\n * ## Placement (decision)\n * Designed to sit at the **end of the tab strip** (Word's position) via\n * `RibbonTabList`'s `actions` prop — which renders it *outside* the `role=\"tablist\"`\n * (so it never trips axe's `aria-required-children`, the same lesson as the\n * tab-strip `⌄`) **and** outside the tab-strip overflow budget (so it never folds).\n * It also works **standalone** anywhere inside a `<Ribbon>` (it only reads context).\n * Both keep axe green — the trigger is an icon-only `<button>` with an `aria-label`\n * (\"Opções de exibição da faixa\"); it is never a tab.\n *\n * The menu **stays open** on selection (checkbox/radio `closeOnClick` defaults to\n * `false`), so a user can flip several display options at once (a settings-panel\n * idiom) and watch the markers update; Escape / outside-click dismiss it.\n */\nfunction RibbonLayoutSwitcher({ className }: RibbonLayoutSwitcherProps) {\n  const { layout, collapsed, autoAdjust, setLayout, setCollapsed, setAutoAdjust } =\n    useRibbonContext();\n  const isClassic = layout === \"classic\";\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        render={\n          <button\n            type=\"button\"\n            data-slot=\"ribbon-layout-switcher-trigger\"\n            aria-label=\"Opções de exibição da faixa\"\n            title=\"Opções de exibição da faixa\"\n            className={cn(\n              \"flex h-9 shrink-0 items-center justify-center rounded-md px-2 text-muted-foreground\",\n              \"cursor-pointer outline-none transition-colors duration-fast ease-ease\",\n              \"hover:bg-accent hover:text-foreground active:bg-accent/80 dark:hover:bg-accent/50\",\n              \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n              className\n            )}\n          >\n            <ChevronGlyph className=\"size-4\" />\n          </button>\n        }\n      />\n      <DropdownMenuContent\n        align=\"end\"\n        className=\"min-w-64\"\n        data-slot=\"ribbon-layout-switcher-menu\"\n      >\n        <DropdownMenuGroup>\n          <DropdownMenuLabel>Layout da Faixa de Opções</DropdownMenuLabel>\n          <DropdownMenuRadioGroup\n            value={layout}\n            onValueChange={(value) => setLayout(normalizeLayout(value as string))}\n          >\n            <DropdownMenuRadioItem value=\"classic\">\n              Faixa de Opções Clássica\n            </DropdownMenuRadioItem>\n            <DropdownMenuRadioItem value=\"single-line\">\n              Faixa de Opções de Linha Única\n            </DropdownMenuRadioItem>\n          </DropdownMenuRadioGroup>\n        </DropdownMenuGroup>\n\n        <DropdownMenuSeparator />\n\n        <DropdownMenuGroup>\n          <DropdownMenuLabel>Mostrar Faixa de Opções</DropdownMenuLabel>\n          <DropdownMenuRadioGroup\n            value={collapsed ? \"tabs-only\" : \"shown\"}\n            onValueChange={(value) => setCollapsed(value === \"tabs-only\")}\n          >\n            <DropdownMenuRadioItem value=\"shown\">\n              Sempre mostrar faixa de opções\n            </DropdownMenuRadioItem>\n            <DropdownMenuRadioItem value=\"tabs-only\">\n              Mostrar apenas as guias\n            </DropdownMenuRadioItem>\n          </DropdownMenuRadioGroup>\n          <DropdownMenuCheckboxItem\n            checked={autoAdjust}\n            disabled={!isClassic}\n            onCheckedChange={(next) => setAutoAdjust(next)}\n          >\n            Ajustar automaticamente\n          </DropdownMenuCheckboxItem>\n        </DropdownMenuGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/* RibbonSeparator                                                              */\n/* -------------------------------------------------------------------------- */\n\n/**\n * RibbonSeparator — a standalone vertical divider for the row (kit\n * `ToolbarSeparator`). Used inside `RibbonGroup` (wrapped in an `OverflowDivider`)\n * and available on its own for ad-hoc separation outside a group.\n */\nfunction RibbonSeparator({\n  className,\n  ...props\n}: ComponentProps<typeof ToolbarSeparator>) {\n  return (\n    <ToolbarSeparator\n      data-slot=\"ribbon-separator\"\n      className={cn(className)}\n      {...props}\n    />\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/* Classic form parts (RibbonLargeButton / RibbonRow / RibbonColumn)           */\n/* -------------------------------------------------------------------------- */\n\nexport interface RibbonLargeButtonProps extends ComponentProps<\"button\"> {\n  /** The ~24px icon shown above the label. */\n  icon?: ReactNode;\n  /** Append a down chevron (for a menu/split composition — see the doc comment). */\n  chevron?: boolean;\n  /**\n   * Base UI-style **element override** — swap the underlying `<button>` for\n   * another element/component while `RibbonLargeButton` keeps ownership of the\n   * large-stacked styling and content. This is the **classic-roving** hook: pass\n   * `render={<ToolbarButton variant=\"ghost\" />}` so the large button becomes a\n   * `Toolbar.Button` composite item and joins the band's arrow-roving order (a\n   * bare `<button>` is skipped by Base UI's roving-tabindex). Chain it for a\n   * roving menu trigger: `render={<ToolbarButton render={<DropdownMenuTrigger />} />}`.\n   * `RibbonLargeButton`'s classes are applied **last** in the merge, so they win\n   * over the target's own (e.g. `ToolbarButton`'s `ghost` `h-8`). Default: a plain\n   * `<button>` (works anywhere — a collapsed-group flyout, a menu trigger, or\n   * standalone — where a hard `Toolbar.Button` dependency would throw).\n   */\n  render?: ReactElement;\n}\n\n/**\n * RibbonLargeButton — the classic band's **large stacked button** (Word's\n * \"Colar\"): a ~24px icon over a (possibly two-line) label, ~68px tall, Fluent\n * *subtle* (ghost) look. A real `<button>` that forwards `ref` + props (React 19),\n * so it composes as a menu trigger via the Base UI `render` idiom\n * (`<DropdownMenuTrigger render={<RibbonLargeButton chevron … />} />`) or a split\n * button — set `chevron` to show the down-caret in those compositions. Icon +\n * label are laid out for you; pass the label as children.\n *\n * ## Classic roving (C4)\n * A bare `<button>` sitting in the classic `Toolbar` is **not** a Base UI\n * `Toolbar.Button` composite item, so the toolbar's arrow-key roving skips it and\n * it becomes a stray extra tab stop (breaking the single-tab-stop APG contract).\n * The fix is the `render` prop: `render={<ToolbarButton variant=\"ghost\" />}` makes\n * this element a composite item so arrow keys reach it, while the large styling\n * still wins the class merge. This can't be automatic — the SAME element renders\n * inside a collapsed group's `Popover` flyout too, and a hard `Toolbar.Button`\n * dependency throws outside a `Toolbar.Root` — so composition is opt-in (the\n * `Toolbar` context flows through the flyout portal, so the composed form is safe\n * in both the band and the flyout). See the preview and `ribbon.test.tsx`.\n */\nfunction RibbonLargeButton({\n  className,\n  icon,\n  chevron = false,\n  children,\n  render,\n  ...props\n}: RibbonLargeButtonProps) {\n  // Large classes come LAST so they win the tailwind-merge over a composed\n  // target's own look (e.g. ToolbarButton's ghost `h-8`); the caller's own\n  // `className` still overrides via `cn` order below.\n  const classes = cn(\n    \"flex h-[68px] w-16 shrink-0 cursor-pointer flex-col items-center justify-start gap-1 rounded-md px-1 py-1.5 text-xs font-normal text-foreground select-none\",\n    \"outline-none transition-colors duration-fast ease-ease\",\n    \"hover:bg-accent hover:text-accent-foreground active:bg-accent/80 dark:hover:bg-accent/50 dark:active:bg-accent/70\",\n    \"disabled:pointer-events-none disabled:opacity-50\",\n    \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n    \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n    className\n  );\n\n  const content = (\n    <>\n      {icon != null ? (\n        <span className=\"flex items-center justify-center [&_svg:not([class*='size-'])]:size-6\">\n          {icon}\n        </span>\n      ) : null}\n      <span className=\"flex max-w-full items-center gap-0.5 text-center leading-tight\">\n        <span className=\"line-clamp-2\">{children}</span>\n        {chevron ? <ChevronGlyph /> : null}\n      </span>\n    </>\n  );\n\n  // `render` = element override (Base UI idiom). Clone it with our styling +\n  // props + the stacked content, merging its own ref/className. The target (e.g.\n  // ToolbarButton) applies our `classes` LAST in its own `cn`, so they win.\n  if (render && isValidElement(render)) {\n    const renderProps = render.props as {\n      className?: string;\n      ref?: PossibleRef<HTMLElement>;\n    };\n    return cloneElement(\n      render,\n      {\n        ...props,\n        \"data-slot\": \"ribbon-large-button\",\n        className: cn(renderProps.className, classes),\n        ref: mergeRefs<HTMLElement>(\n          renderProps.ref,\n          (props as { ref?: PossibleRef<HTMLElement> }).ref\n        ),\n      } as Record<string, unknown>,\n      content\n    );\n  }\n\n  return (\n    <button data-slot=\"ribbon-large-button\" className={classes} {...props}>\n      {content}\n    </button>\n  );\n}\n\n/**\n * RibbonRow — a tiny horizontal flex helper (`data-slot=\"ribbon-row\"`) for\n * composing a group's controls, e.g. one row of a 2×3 small-icon-button grid.\n * Forwards `ref` + props (React 19).\n */\nfunction RibbonRow({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"ribbon-row\"\n      className={cn(\"flex items-center gap-0.5\", className)}\n      {...props}\n    />\n  );\n}\n\n/**\n * RibbonColumn — a tiny vertical flex helper (`data-slot=\"ribbon-column\"`) for\n * stacking `RibbonRow`s (the 2×3 grid) or a vertical list of medium icon+label\n * buttons. Forwards `ref` + props (React 19).\n */\nfunction RibbonColumn({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"ribbon-column\"\n      className={cn(\"flex flex-col gap-0.5\", className)}\n      {...props}\n    />\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/* RibbonOverflowMenu (the \"…\" trigger + grouped menu)                         */\n/* -------------------------------------------------------------------------- */\n\n// Kept measurable while hidden (out of flow + invisible), NOT display:none, so\n// the manager can still read the trigger's width to reserve it from the budget\n// (overflow.tsx's `useOverflowMenu` documents this requirement).\nconst HIDDEN_TRIGGER_STYLE: CSSProperties = {\n  position: \"absolute\",\n  right: 4,\n  visibility: \"hidden\",\n  pointerEvents: \"none\",\n};\n\n/** Fluent-style \"…\" glyph — inline SVG (no icon dependency), à la split-button. */\nfunction MoreGlyph() {\n  return (\n    <svg viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\" className=\"size-4\">\n      <circle cx=\"5\" cy=\"10\" r=\"1.4\" />\n      <circle cx=\"10\" cy=\"10\" r=\"1.4\" />\n      <circle cx=\"15\" cy=\"10\" r=\"1.4\" />\n    </svg>\n  );\n}\n\n/**\n * RibbonOverflowMenu — the \"…\" trigger + its menu. Rendered automatically by\n * `RibbonContent` (you don't place it). The trigger is a ghost icon-only\n * `ToolbarButton` wired via `useOverflowMenu` (kept measurable-but-hidden until\n * something overflows). The menu renders **only hidden** items, grouped: a\n * `DropdownMenuLabel` header per group whose items include a hidden one, then\n * each hidden item's menu form (default `DropdownMenuItem`, or its custom\n * `overflowRender`), source order preserved, separators between groups.\n */\nfunction RibbonOverflowMenu() {\n  const { registry, overflowTriggerRef } = useRibbonContentContext();\n  const { ref, isOverflowing, overflowCount } =\n    useOverflowMenu<HTMLButtonElement>();\n\n  const items = useSyncExternalStore(\n    registry.subscribe,\n    registry.getSnapshot,\n    registry.getServerSnapshot\n  );\n\n  // Group items in first-seen (source) order. Ungrouped items collapse into a\n  // single trailing headerless section.\n  const sections = useMemo(() => {\n    const order: string[] = [];\n    const map = new Map<\n      string,\n      { groupId?: string; groupLabel?: string; items: RibbonItemMeta[] }\n    >();\n    for (const item of items) {\n      const key = item.groupId ?? \"__ungrouped__\";\n      let entry = map.get(key);\n      if (!entry) {\n        entry = {\n          groupId: item.groupId,\n          groupLabel: item.groupLabel,\n          items: [],\n        };\n        map.set(key, entry);\n        order.push(key);\n      }\n      entry.items.push(item);\n    }\n    return order.map((key) => map.get(key)!);\n  }, [items]);\n\n  const triggerRef = useCallback(\n    (el: HTMLButtonElement | null) => {\n      ref(el);\n      overflowTriggerRef.current = el;\n    },\n    [ref, overflowTriggerRef]\n  );\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        render={\n          <ToolbarButton\n            ref={triggerRef}\n            size=\"icon\"\n            data-slot=\"ribbon-overflow-trigger\"\n            data-overflow-menu=\"\"\n            aria-label={\n              overflowCount === 1\n                ? \"1 more command\"\n                : `${overflowCount} more commands`\n            }\n            className={isOverflowing ? \"ml-auto\" : undefined}\n            style={isOverflowing ? undefined : HIDDEN_TRIGGER_STYLE}\n          >\n            <MoreGlyph />\n          </ToolbarButton>\n        }\n      />\n      <DropdownMenuContent\n        align=\"end\"\n        className=\"min-w-56\"\n        data-slot=\"ribbon-overflow-menu\"\n      >\n        {sections.map((section, index) =>\n          section.groupId ? (\n            <RibbonOverflowGroupSection\n              key={section.groupId}\n              groupId={section.groupId}\n              groupLabel={section.groupLabel}\n              items={section.items}\n              index={index}\n            />\n          ) : (\n            <RibbonOverflowUngroupedSection\n              key=\"__ungrouped__\"\n              items={section.items}\n              index={index}\n            />\n          )\n        )}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\n/** A grouped section — header + hidden rows, shown only when the group isn't fully visible. */\nfunction RibbonOverflowGroupSection({\n  groupId,\n  groupLabel,\n  items,\n  index,\n}: {\n  groupId: string;\n  groupLabel?: string;\n  items: RibbonItemMeta[];\n  index: number;\n}) {\n  const state = useIsOverflowGroupVisible(groupId);\n  if (state === \"visible\") return null;\n  return (\n    <>\n      {index > 0 ? <DropdownMenuSeparator /> : null}\n      <DropdownMenuGroup>\n        {groupLabel ? <DropdownMenuLabel>{groupLabel}</DropdownMenuLabel> : null}\n        {items.map((item) => (\n          <RibbonOverflowRow key={item.id} meta={item} />\n        ))}\n      </DropdownMenuGroup>\n    </>\n  );\n}\n\n/** Ungrouped items — no header; each row self-hides while visible. */\nfunction RibbonOverflowUngroupedSection({\n  items,\n  index,\n}: {\n  items: RibbonItemMeta[];\n  index: number;\n}) {\n  return (\n    <>\n      {index > 0 ? <DropdownMenuSeparator /> : null}\n      <DropdownMenuGroup>\n        {items.map((item) => (\n          <RibbonOverflowRow key={item.id} meta={item} />\n        ))}\n      </DropdownMenuGroup>\n    </>\n  );\n}\n\n/** One command, rendered only while it is overflowed (hidden in the row). */\nfunction RibbonOverflowRow({ meta }: { meta: RibbonItemMeta }) {\n  const visible = useIsOverflowItemVisible(meta.id);\n  if (visible) return null;\n  if (meta.overflowRender) return <>{meta.overflowRender()}</>;\n  return (\n    <DropdownMenuItem\n      onClick={() => meta.onSelect?.()}\n    >\n      {meta.icon}\n      {meta.label}\n    </DropdownMenuItem>\n  );\n}\n\nexport {\n  Ribbon,\n  RibbonTabList,\n  RibbonTab,\n  RibbonContent,\n  RibbonGroup,\n  RibbonItem,\n  RibbonOverflowMenu,\n  RibbonLayoutSwitcher,\n  RibbonSeparator,\n  RibbonLargeButton,\n  RibbonRow,\n  RibbonColumn,\n};\n"
    }
  ],
  "docs": "Compose `Ribbon` > `RibbonTabList`/`RibbonTab` + one `RibbonContent` per tab > `RibbonGroup` > `RibbonItem`. `RibbonContent` wires the Overflow provider, the measured Toolbar, the item registry, AND the '…' overflow menu for you — you only supply groups and items.\n\n```tsx\n<Ribbon defaultValue=\"home\">\n  <RibbonTabList aria-label=\"Ribbon\">\n    <RibbonTab value=\"home\">Home</RibbonTab>\n    <RibbonTab value=\"insert\">Insert</RibbonTab>\n  </RibbonTabList>\n  <RibbonContent value=\"home\">\n    <RibbonGroup groupId=\"font\" label=\"Font\">\n      <RibbonItem id=\"bold\" label=\"Bold\" icon={<BoldIcon />} priority={80} pinned onSelect={bold}>\n        <ToolbarButton size=\"icon\" aria-label=\"Bold\"><BoldIcon /></ToolbarButton>\n      </RibbonItem>\n      {/* …more items */}\n    </RibbonGroup>\n    {/* …more groups */}\n  </RibbonContent>\n</Ribbon>\n```\n\nEvery `RibbonItem` needs `id` + `label` (+ optional `icon`) so it can render as a bar icon AND a labeled menu row. `priority` (higher survives longer, beats DOM order) and `pinned` (never overflows) drive the overflow order. Supply `overflowRender` for a complex menu form (a split button → submenu, a combo → labeled field); otherwise the default menu form is a `DropdownMenuItem` firing `onSelect`. Axes mirror Word's switcher and are each controllable (controlled prop + `default*` + `on*Change`): `layout` ('single-line' or 'classic' — the same tree renders both; unknown values degrade to single-line), `collapsed` ('Mostrar apenas as guias' — tabs-only; activating any tab un-collapses), and `autoAdjust` (classic adaptive resize). Drop `<RibbonLayoutSwitcher />` into `<RibbonTabList actions={…}>` for Word's far-right display-options chevron; it reads/writes all three axes through context and is placed outside the tablist + the tab-overflow budget so it never folds and stays axe-clean. In classic, give each RibbonGroup an `icon` + `collapsePriority` (higher collapses first) and optionally a launcher (`onLauncherClick` or `launcher`); compose group content with RibbonLargeButton / RibbonRow / RibbonColumn. Use `layouts` on a RibbonItem/RibbonGroup to scope it to one layout. There is no ARIA 'ribbon' pattern: the composite is APG Tabs over APG Toolbar, and each command row is labelled by its owning tab. See ribbon.tsx's doc comment for the full model, the desktop-Office divergence (no overlay flyout when collapsed), and the focus-preservation behavior."
}
