{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toolbar-overflow",
  "title": "Toolbar Overflow",
  "description": "A row of controls that folds whatever will not fit into a menu at its end — the pattern usually called priority+. Reach for it wherever a row of buttons has to survive a width nobody chose: an editor's formatting bar, a table's action column, an article's share row, an admin header, a card's footer actions, a navigation bar that has dropped to a phone, a sidebar somebody can drag narrower, a dashboard panel that resizes with the window, a toolbar that grew one button at a time until it stopped fitting on a laptop. Common asks it answers: \"priority plus pattern react\", \"overflow menu react\", \"toolbar overflow shadcn\", \"responsive toolbar that collapses buttons\", \"move buttons into a dropdown when they do not fit\", \"collapse toolbar items into a more menu\", \"hide toolbar buttons on narrow screens\", \"react overflow set\", \"fluent ui overflowset equivalent shadcn\", \"material toolbar overflow shadcn\", \"three dot menu for the extra actions\", \"kebab menu for overflow items\", \"how many items fit in a container react\", \"measure available width react\", \"ResizeObserver toolbar react\", \"hide overflowing children with ResizeObserver\", \"detect overflowing flex items react\", \"ResizeObserver loop completed with undelivered notifications\", \"ResizeObserver infinite loop when hiding elements\", \"toolbar flickers while resizing\", \"items appear then disappear on load\", \"buttons flash before collapsing on first render\", \"hidden items measure zero width\", \"measure the natural width of a hidden element react\", \"how to measure an element that is display none\", \"overflow button width not accounted for\", \"last item overflows after the more button is added\", \"off by one gap in a flex overflow calculation\", \"flex items shrink instead of overflowing\", \"flex-shrink-0 buttons overflow\", \"role toolbar arrow keys react\", \"roving tabindex toolbar\", \"aria toolbar keyboard navigation\", \"focus lost when an element unmounts\", \"focus jumps to body when the focused button is hidden\", \"tab skips my toolbar\", \"no tab stop left in a roving tabindex group\", \"arrow keys stop working after a resize\", \"overflow menu accessible name\", \"aria-haspopup menu button react\", \"escape should close the overflow menu\", \"rtl toolbar arrow keys reversed\", \"icon only toolbar button accessible name\", \"ツールバー 折りたたみ react\", \"入りきらないボタンをメニューに畳む\", \"その他メニュー 実装\", \"レスポンシブ ツールバー ボタン\", \"幅に応じてボタンを隠す\", \"ResizeObserver 無限ループ\", \"測ってから隠すとちらつく\", \"role=toolbar 矢印キー\", \"roving tabindex フォーカスが飛ぶ\", \"三点メニュー 余りの操作\", \"フォーカスしていた項目が隠れると Tab が飛ぶ\". Official shadcn/ui ships both halves of this and nothing that measures. Measured across its whole registry today — 63 items listed, 62 fetchable, `questionnaire` indexed and 404 on both style tracks, 223,287 bytes of `files[].content` concatenated, the fifth consecutive day at that exact figure — `ResizeObserver`, `getBoundingClientRect`, `offsetWidth`, `IntersectionObserver`, `requestAnimationFrame`, `overflowItems`, `priorityPlus`, `roving`, `toolbar` and `role=\"toolbar\"` are every one of them a zero hit. There is one `aria-orientation` in the whole registry and it is not an attribute: it is a Tailwind variant selector, `aria-[orientation=horizontal]:w-full` and five of its siblings, styling the handle in `resizable` (1,805 bytes) whose actual attribute comes from `react-resizable-panels`, an npm dependency — the same shape as the single `aria-selected` that turns out to be a `calendar` variant. So the registry contains no file that measures an element and no file that writes a toolbar's keyboard, and the two pieces it does ship are the two halves of the problem: `button-group` (2,203 bytes, `cn` and nothing else) joins the buttons visually, and `dropdown-menu` (8,408 bytes) is the menu, handed to `radix-ui` as an npm dependency — which is also why grepping official for `aria-haspopup`, `role=\"menu\"` or `menuitem` proves nothing in either direction, since Radix supplies them at runtime. `menubar` (8,379) and `navigation-menu` (6,648) are Radix wrappers too, and both are navigation rather than folding. Nothing in the registry has a route to \"how many of these fit\", so everybody writes that part by hand, and **measuring your own row is a loop unless you arrange otherwise.** The reflex implementation asks the live row how wide it is, hides what sticks out, and is then asked again by its own ResizeObserver: the hidden controls now measure zero, so the row fits, so they come back, so it overflows, so they are hidden — and because every pass changes layout, the observer fires again. That is the flicker behind \"ResizeObserver loop completed with undelivered notifications\", and no amount of debouncing fixes it, because every pass is a correct answer to a question that should not have been asked. Two things break it here. Natural widths come from a probe that is never folded — a hidden copy of the full set, absolutely positioned and `visibility: hidden` rather than `display: none` so it still has a box to measure, on one line with `width: auto` and `max-width: none` so what it reports is the width each control wants rather than the width it was given. And the container's width cannot be changed by its contents: the root is a block-level, full-width box with `overflow-hidden`, so its width comes from its parent and a row one pixel too wide is clipped rather than pushing anything wider. Together they make the visible count a pure function of inputs that folding does not touch, so the same container width always produces the same count, React bails out of a state update to an equal value, and the observer settles after one pass — the loop is removed rather than damped. The flash is a separate problem: the measurement runs in a layout effect, after layout and before paint, so the full row is never painted and then collapsed. The honest limit is the server, which cannot measure, so an SSR'd page shows the row clipped until hydration rather than wrapped or overflowing. Four more things are easy to get wrong and are each written down in the source. The trigger's own width is charged if and only if something folds: reserve it always and a toolbar whose controls fit exactly loses its last one to a button that was never needed, reserve it never and the control that \"fits\" is pushed out by the trigger added after the decision, so the row overflows by exactly the width of the thing meant to prevent overflow. The gaps go with it — `n` controls alone have `n - 1` gaps, `k` controls plus a trigger have `k`, because the gap in front of the trigger is real. The controls carry `shrink-0`, which is load-bearing rather than cosmetic: a flex item that may shrink does, so without it the browser squeezes everything into unreadable slivers, the row never overflows, and the component silently does nothing. And when the row narrows, two different things go wrong with the tab stop, only one of which is about focus — the visible failure is that the control holding focus leaves the document and a browser does not move focus when that happens, it drops to `<body>`, so the next Tab starts from the top of the page; the quieter one happens whether or not anybody is looking, because a roving tabindex puts `tabIndex={0}` on exactly one control and if that control is now in the menu then nothing in the row has it and Tab skips the whole toolbar. So the stop moves on every measurement and focus is only *called* when focus was already inside, since a window being dragged narrower is not a reason to take focus away from whatever somebody is doing elsewhere on the page. The keyboard is two conventions meeting at the trigger. The row is a toolbar: one tab stop, Left and Right between controls and swapped on an RTL page, Home and End to the ends, clamping — which is the practices' description of a toolbar and the opposite of the menu underneath, where the arrows wrap. The menu is a menu: Up and Down with wrapping, Escape back to the trigger, and choosing an item returns focus there too. They meet at the trigger, the last stop in the row and the thing Down opens, so Right, Right, Down walks out of the row and into the menu and Escape, Left walks back. A horizontal arrow pressed inside the open menu is swallowed rather than left to bubble: letting it through moves the row's tab stop underneath a menu somebody is still reading, and Escape then returns focus to a control they never left. What you get: `ToolbarOverflow`, with `label` required because a `role=\"toolbar\"` needs an accessible name and because the trigger's name is built from it — \"2 more Formatting actions\", so two toolbars on a page are not both \"More\"; `items`, most important first, because **the order is the priority** and the row keeps a prefix while the tail folds, which is what keeps the menu in the same order as the row somebody last saw (there is deliberately no per-item priority flag: it would make the visible set an arbitrary subset, turn the arithmetic into a knapsack, and leave an item nowhere near where it was); each item `{ label, onSelect, icon?, disabled?, hideLabel?, id? }`, where `disabled` becomes `aria-disabled` and never the `disabled` attribute, so an unavailable control keeps its place in the arrow walk instead of making the walk go 1, 2, 4, and `hideLabel` shows the icon alone in the row with the label as both the accessible name and the native tooltip while the menu always shows the words. `align` picks which edge the menu lines up with and mirrors itself on an RTL page. `onVisibleCountChange` reports how many controls are in the row after each measurement, and `onOpenChange` is reported from an effect rather than from inside a state setter, so a caller is told once per change instead of twice under strict mode. `fitToolbarItems` is the whole decision as a plain function of widths, available width, gap and trigger width — the part worth asserting, since every interesting case here is a width rather than a click — and `nextToolbarStop` and `rescueToolbarStop` are the row's clamping walk and the tab-stop rescue, each on its own. Per-part `className` for the row, the controls, the trigger, the panel and its items. Nothing is portalled and nothing measures the viewport: the menu is absolutely positioned inside the row, which is what a short menu under a button needs. One file, one lucide icon, and no `radix-ui`. Distinct from the neighbours: `scroll-shadow` says there is more in a scrollable row rather than folding it, and `middle-truncate` shortens one string to fit rather than deciding which whole controls survive — it is the other component here that measures text against a box, and the probe below is the same idea at the scale of a row. `split-button` is one action with its variants under an arrow beside it, and this composes with it: the overflow menu's wrap rule is `split-button`'s own `nextMenuIndex`, imported rather than written twice, so the two menus in this registry cannot drift apart. `bulk-action-bar` is a fixed set of actions over a selection, `keyboard-shortcuts` is the sheet that lists what the keys do, `command-palette` is a searchable dialog over everything reached by a shortcut, and `filter-chips`, `segmented-control` and `multi-select` all pick values rather than running actions. `column-resizer` and `sticky-table-header` measure a table; this measures itself.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://pulld.pages.dev/r/split-button.json"
  ],
  "files": [
    {
      "path": "registry/ui/toolbar-overflow.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { MoreHorizontal } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { nextMenuIndex } from \"@/registry/ui/split-button\"\n\n/**\n * Sub-pixel slack, the same half a pixel `middle-truncate` allows itself and for the same reason:\n * element widths and content-box widths are both fractional, so a row that fills its container\n * exactly can report a few hundredths of a pixel of overflow. Kept well under one pixel on purpose\n * — generous slack folds an item that would have fitted, which is the one outcome this component\n * exists to avoid.\n */\nconst WIDTH_TOLERANCE = 0.5\n\n/** The stop that is the overflow trigger rather than one of the items. */\nexport const TOOLBAR_MORE = -1\n\n/**\n * How many leading items stay in the row.\n *\n * This is the whole decision, as arithmetic over numbers that nothing in the DOM can argue with,\n * and it is separate from the component for two reasons. The first is that it is the part worth\n * asserting: every interesting case here is a width, not a click. The second is that keeping it a\n * function of its arguments alone is what stops the component oscillating — see\n * {@link ToolbarOverflow} for the loop this shape rules out.\n *\n * `widths` are the items' **natural** widths, in the caller's order, which is also the priority\n * order: the row keeps a prefix and folds the tail. `available` is the container's content width,\n * `gap` the row's column gap, `moreWidth` the natural width of the trigger.\n *\n * The trigger's own width is the part that is easy to get wrong, and it is wrong in both\n * directions. Reserve room for it always and a toolbar whose items fit exactly loses its last item\n * to a button that was never needed; reserve it never and the last item that \"fits\" is pushed out\n * by the trigger added after the decision — the row then overflows by exactly the width of the\n * thing that was supposed to prevent overflow. So it is charged if and only if something folds,\n * which is why this asks whether everything fits *before* it asks how much fits.\n *\n * The gap count changes with it. `n` items alone have `n - 1` gaps between them; `k` items plus a\n * trigger have `k` — the trigger is a box in the row like any other, and the gap in front of it is\n * real. Counting `k - 1` there is the quiet version of the same mistake, one gap wide.\n */\nexport function fitToolbarItems(\n  widths: readonly number[],\n  available: number,\n  gap: number,\n  moreWidth: number\n): number {\n  const count = widths.length\n  if (count === 0) return 0\n\n  // A negative or non-finite container is \"nothing fits\" rather than a crash: `available` arrives\n  // from a measurement, and a detached or display:none element measures 0.\n  const room = Number.isFinite(available) ? available : 0\n  const step = Number.isFinite(gap) && gap > 0 ? gap : 0\n  const sizes = widths.map((w) => (Number.isFinite(w) && w > 0 ? w : 0))\n\n  let total = step * (count - 1)\n  for (const width of sizes) total += width\n  if (total - room <= WIDTH_TOLERANCE) return count\n\n  // Past here something folds, so the row carries the trigger too and every candidate pays for it.\n  const trigger = Number.isFinite(moreWidth) && moreWidth > 0 ? moreWidth : 0\n  let used = trigger\n  let kept = 0\n  // `count - 1` rather than `count`: keeping all of them is the case above, and reaching it here\n  // would mean charging for a trigger that is not rendered.\n  for (let i = 0; i < count - 1; i++) {\n    const next = used + step + sizes[i]\n    if (next - room > WIDTH_TOLERANCE) break\n    used = next\n    kept = i + 1\n  }\n  return kept\n}\n\n/**\n * Where a horizontal arrow key lands in the row, clamping at both ends.\n *\n * The stops are the visible items followed by the trigger, and `TOOLBAR_MORE` names the last one\n * because its index moves every time the container resizes — `visible` today is `visible - 1`\n * tomorrow, and a caller holding the number rather than the sentinel is holding a stale answer.\n *\n * Clamping, where the menu that opens underneath wraps. That is not an inconsistency: the ARIA\n * practices describe a toolbar's arrows as moving between its controls with wrapping optional, and\n * the two popups differ in what the person can see. A three-item menu clamped makes its last item a\n * key that does nothing, and since the menu appeared a moment ago there is nothing on screen to say\n * why — it reads as the keyboard having broken. A toolbar's ends are the ends of a row that is\n * visibly there, so Right doing nothing on the last control reads as the edge of the row. The wrap\n * rule for the menu is imported from `split-button` rather than written again here, so the two\n * menus in this registry cannot drift apart.\n *\n * `delta` of 0 is how Home and End are expressed: Home is \"from the first stop, don't move\" and End\n * is \"from the trigger, don't move\", which resolves to the last item when nothing is folded.\n */\nexport function nextToolbarStop(\n  visible: number,\n  hasMore: boolean,\n  from: number,\n  delta: number\n): number {\n  const span = Number.isFinite(visible) ? Math.max(0, Math.trunc(visible)) : 0\n  const stops = span + (hasMore ? 1 : 0)\n  // No stops at all: an empty toolbar renders nothing, and this is what its keys answer.\n  if (stops < 1) return TOOLBAR_MORE\n  const step = Number.isFinite(delta) ? Math.trunc(delta) : 0\n  const current =\n    from === TOOLBAR_MORE\n      ? // The trigger sits after the last visible item; without one, \"the end\" is that item.\n        span - (hasMore ? 0 : 1)\n      : Math.min(Math.max(Math.trunc(Number.isFinite(from) ? from : 0), 0), stops - 1)\n  const next = Math.min(Math.max(current + step, 0), stops - 1)\n  return hasMore && next === span ? TOOLBAR_MORE : next\n}\n\n/**\n * The stop that should hold the row's single tab stop, given the one that holds it now.\n *\n * Returns `active` unchanged when there is nothing to rescue, so the caller acts on a change and\n * not on every measurement.\n *\n * Two things go wrong when the container narrows, and only one of them is about focus. The visible\n * one is that the item holding focus is folded away: its button leaves the document, and a browser\n * does not move focus when that happens — it drops to `<body>`, so the next Tab starts again from\n * the top of the page. The quieter one happens whether or not anybody is looking: a roving tabindex\n * puts `tabIndex={0}` on exactly one control, and if that control is now in the menu then **no\n * element in the row has it**, so Tab skips the entire toolbar. That second failure needs no focus\n * to be present and no screen reader to notice, which is why the tab stop is moved on every\n * measurement and focus is only *called* when focus was already inside — see {@link\n * ToolbarOverflow}, where moving focus unasked would take it from whatever the person was doing\n * elsewhere on the page while the window happened to be resizing.\n *\n * The reverse case is the same rule read backwards: the row widens, everything fits, the trigger is\n * gone, and the tab stop is on a control that no longer exists. It lands on the last item in the\n * row, which is the position the trigger occupied.\n */\nexport function rescueToolbarStop(active: number, visible: number, total: number): number {\n  const count = Number.isFinite(total) ? Math.max(0, Math.trunc(total)) : 0\n  if (count === 0) return TOOLBAR_MORE\n  const span = Math.min(Math.max(Number.isFinite(visible) ? Math.trunc(visible) : 0, 0), count)\n  const hasMore = span < count\n\n  if (active === TOOLBAR_MORE) return hasMore ? TOOLBAR_MORE : Math.max(0, span - 1)\n  const index = Math.min(Math.max(Math.trunc(Number.isFinite(active) ? active : 0), 0), count - 1)\n  // `index >= span` means the item is folded, and a folded item implies a trigger exists to receive\n  // the tab stop: there is no way to be past the visible prefix without the prefix being short.\n  return index >= span ? TOOLBAR_MORE : index\n}\n\n/**\n * Whether an element is laid out right-to-left.\n *\n * Read from the node at the moment of the key press rather than cached, because `direction` is\n * inherited and can change underneath — an `<html dir>` toggle, one Arabic toolbar in an otherwise\n * English page — and a stale answer sends Right to the left for the rest of the session.\n */\nfunction isRtl(node: Element | null | undefined): boolean {\n  if (!node || typeof getComputedStyle !== \"function\") return false\n  return getComputedStyle(node).direction === \"rtl\"\n}\n\n// Measures before paint, so the full row is never briefly visible before it folds. It warns during\n// SSR, where there is nothing to measure.\nconst useIsomorphicLayoutEffect =\n  typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect\n\nexport interface ToolbarOverflowItem {\n  /** Stable key. The label is used when this is absent. */\n  id?: string\n  /** The control's text, and its accessible name. */\n  label: string\n  /** Run when the control is pressed. Not called for a disabled item. */\n  onSelect: () => void\n  /** Decorative icon before the label. Hidden from screen readers by this component. */\n  icon?: React.ReactNode\n  /**\n   * Unavailable, but still shown and still reachable with the arrow keys.\n   *\n   * `aria-disabled` rather than the `disabled` attribute, the same choice `split-button` makes: a\n   * `disabled` button is out of the focus order, so arrowing a toolbar of five whose third is\n   * unavailable walks 1, 2, 4, 5 — and what the person learns from that is not that the control is\n   * unavailable but that it does not exist.\n   */\n  disabled?: boolean\n  /**\n   * Show the icon alone in the row, with the label as the accessible name and the native tooltip.\n   *\n   * For the formatting toolbars this pattern comes from, where bold and italic are a B and an I. The\n   * menu always shows the text, whatever this says, because a row of icons folded into a menu of\n   * icons is a menu of unlabelled squares.\n   */\n  hideLabel?: boolean\n}\n\nexport interface ToolbarOverflowLabels {\n  /**\n   * The trigger's accessible name, from the number folded and the toolbar's own name.\n   *\n   * A function rather than a string because the name has to carry both. A page with a formatting\n   * toolbar and a table toolbar has two triggers, and two announcements of \"More\" tell a\n   * screen-reader user only that there are two of something — the same reason `split-button` builds\n   * its arrow's name from the action beside it. The count is in there because it is the one piece of\n   * information the trigger has that nothing else on screen does.\n   */\n  more: (count: number, label: string) => string\n}\n\nexport const defaultToolbarOverflowLabels: ToolbarOverflowLabels = {\n  more: (count, label) => `${count} more ${label} actions`,\n}\n\nconst CONTROL_BASE =\n  // shrink-0 is load-bearing rather than cosmetic: a flex item that may shrink does, so without it\n  // the browser squeezes every control into an unreadable sliver and the row never overflows, which\n  // means nothing ever folds and this component silently does nothing.\n  \"inline-flex h-8 shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md \" +\n  \"px-2.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 \" +\n  \"focus-visible:ring-ring hover:bg-accent hover:text-accent-foreground\"\n\n/**\n * The probe takes part in no layout: absolutely positioned, so it contributes nothing to the row's\n * width, and `visibility: hidden` rather than `display: none` because a display:none box reports\n * zero and the point of it is to be measurable.\n */\nconst PROBE_STYLE: React.CSSProperties = {\n  position: \"absolute\",\n  top: 0,\n  left: 0,\n  display: \"flex\",\n  visibility: \"hidden\",\n  pointerEvents: \"none\",\n  whiteSpace: \"nowrap\",\n  width: \"auto\",\n  maxWidth: \"none\",\n  userSelect: \"none\",\n}\n\nexport interface ToolbarOverflowProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\" | \"aria-label\"> {\n  /**\n   * What the toolbar is called — \"Formatting\", \"Table\", \"Row actions\".\n   *\n   * Required, because a `role=\"toolbar\"` with no accessible name is announced as \"toolbar\" and the\n   * practices ask for a name. It is also where the trigger's name comes from, so the usual call\n   * site passes it once and gets both.\n   */\n  label: string\n  /**\n   * The controls, most important first. **The order is the priority:** the row keeps a prefix and\n   * folds the tail.\n   *\n   * There is deliberately no per-item priority flag. Allowing one would make the visible set an\n   * arbitrary subset rather than a prefix, which turns the arithmetic into a knapsack and — the part\n   * that actually matters — makes the menu a list of items in no relation to the row, so the place\n   * an item went is no longer where the person last saw it. Putting the important ones first says\n   * the same thing and keeps both halves in one order.\n   */\n  items: readonly ToolbarOverflowItem[]\n  /** Which edge of the toolbar the overflow menu lines up with. Mirrors itself on an RTL page. */\n  align?: \"start\" | \"end\"\n  /** Told when the overflow menu opens and closes. */\n  onOpenChange?: (open: boolean) => void\n  /**\n   * Told how many controls are in the row after each measurement, for a caller that wants to know —\n   * a tooltip that should not be attached to a control nobody can see, an analytics note about how\n   * often the toolbar is cramped.\n   */\n  onVisibleCountChange?: (visible: number) => void\n  labels?: Partial<ToolbarOverflowLabels>\n  /**\n   * On the row — the element with `role=\"toolbar\"`, which is what anybody looking at the page would\n   * call the toolbar. The outer box above it is a positioning wrapper with no styles of its own and\n   * is deliberately not reachable: it exists so the menu is not clipped, and giving it a width or a\n   * padding is how the measurement starts lying. `...props` reach the row too, for the same reason.\n   *\n   * This is also where the spacing is said: `className=\"gap-2\"` is read back out of the row's\n   * computed style and used in the arithmetic, so the gap is written once rather than twice.\n   */\n  className?: string\n  itemClassName?: string\n  triggerClassName?: string\n  menuClassName?: string\n  menuItemClassName?: string\n}\n\n/**\n * A row of controls that folds whatever will not fit into a menu at its end — the pattern usually\n * called priority+.\n *\n * For an editor's formatting bar, a table's action column, an article's share row, an admin\n * header, a card's footer actions, a nav that has dropped to a narrow screen: anywhere a row of\n * buttons has to survive a width nobody chose.\n *\n * **Measuring your own row is a loop unless you arrange otherwise,** and that is the whole design.\n * The reflex implementation asks the live row how wide it is, hides what sticks out, and is then\n * asked again by its own ResizeObserver: the hidden controls now measure zero, so the row fits, so\n * they come back, so it overflows, so they are hidden — a flicker that no amount of debouncing\n * fixes, because every pass is a legitimate answer to a question that should not have been asked.\n * Two things break it here:\n *\n *   - **Natural widths come from a probe that is never folded.** A hidden copy of the full set sits\n *     in the row, out of layout, and is what gets measured. Folding the visible row cannot change\n *     what the probe says.\n *   - **The container's width cannot be changed by its contents.** The row is a block-level,\n *     full-width box with `overflow-hidden`, so its width comes from its parent and a row that is\n *     one pixel too wide is clipped rather than pushing anything wider. (A `w-fit` or `inline-flex`\n *     row is the one way to reintroduce the loop, because then the width being measured is the\n *     content's own.)\n *\n * Together they make the visible count a pure function of inputs that folding does not touch — see\n * {@link fitToolbarItems} — so the same container width always produces the same count, React bails\n * out of a state update to an equal value, and the observer settles after one pass. The loop is not\n * damped; it is removed.\n *\n * The flash is a separate problem with a separate answer: the measurement runs in a layout effect,\n * which is after layout and before paint, so the full row is never painted and then collapsed. The\n * honest limit is the server, which cannot measure — an SSR'd page shows the row clipped by\n * `overflow-hidden` until hydration, rather than wrapped or overflowing.\n *\n * The keyboard is two conventions meeting at the trigger. The row is a toolbar: one tab stop,\n * Left/Right between controls (swapped on an RTL page), Home and End to the ends, clamping. The\n * menu is a menu: Up/Down with wrapping, Escape back to the trigger, and choosing an item returns\n * focus there too. They meet at the trigger, which is the last stop in the row and the thing Down\n * opens — so Right, Right, Down walks out of the row and into the menu, and Escape, Left walks back.\n * A horizontal arrow pressed inside the open menu is swallowed rather than left to bubble: letting\n * it through moves the row's tab stop underneath a menu the person is still reading, and Escape then\n * returns focus somewhere they never left.\n *\n * That clipping is also why this renders two boxes rather than one. The row clips, and an\n * absolutely positioned panel whose containing block is the thing clipping it is painted only where\n * the two overlap — a panel at `top-full` is entirely below the row, so it would be painted nowhere.\n * So the menu hangs off an outer box that positions without clipping, and the row inside it keeps\n * both the clipping and the `role=\"toolbar\"`. The probe stays in the row on purpose: an out-of-flow\n * element still contributes scrollable overflow, and being clipped is what stops a toolbar far too\n * narrow for its controls from making the page scroll sideways.\n *\n * Nothing is portalled and nothing measures the viewport; the menu is absolutely positioned against\n * that outer box. Distinct from `scroll-shadow`, which says there is more in a scrollable row rather\n * than folding it, and from `bulk-action-bar`, which is a fixed set of actions over a selection.\n */\nexport function ToolbarOverflow({\n  label,\n  items,\n  align = \"end\",\n  onOpenChange,\n  onVisibleCountChange,\n  labels: labelOverrides,\n  className,\n  itemClassName,\n  triggerClassName,\n  menuClassName,\n  menuItemClassName,\n  ...props\n}: ToolbarOverflowProps) {\n  const labels = React.useMemo(\n    () => ({ ...defaultToolbarOverflowLabels, ...labelOverrides }),\n    [labelOverrides]\n  )\n  const generatedId = React.useId()\n  const menuId = `${generatedId}-menu`\n  const triggerId = `${generatedId}-trigger`\n\n  const total = items.length\n\n  // null is \"not measured yet\", which is not the same answer as \"nothing fits\": the server cannot\n  // measure and the first client render has to match it, so every render starts with the whole row\n  // and the layout effect below narrows it before paint.\n  const [measured, setMeasured] = React.useState<number | null>(null)\n  const [open, setOpen] = React.useState(false)\n  const [active, setActive] = React.useState(0)\n  // Which menu item has focus, as a position. -1 is \"the menu is closed\", as in `split-button`.\n  const [menuActive, setMenuActive] = React.useState(-1)\n\n  // Two elements, and the split is not cosmetic — see {@link ToolbarOverflow}. `rootRef` is the\n  // outer box the menu is positioned against; `rowRef` is the toolbar itself, and the only one that\n  // clips.\n  const rootRef = React.useRef<HTMLDivElement | null>(null)\n  const rowRef = React.useRef<HTMLDivElement | null>(null)\n  const probeRef = React.useRef<HTMLDivElement | null>(null)\n  const triggerRef = React.useRef<HTMLButtonElement | null>(null)\n\n  /**\n   * One ref object per position, created lazily — object refs rather than callback refs for the\n   * reason `split-button` gives: a callback ref is a new function identity on every commit, so\n   * React detaches and reattaches it, and this repository's test harness fills ref objects only, so\n   * a roving tabindex wired with callbacks has focus behaviour that cannot be asserted at all.\n   */\n  const itemRefs = React.useRef<Array<React.RefObject<HTMLButtonElement | null>>>([])\n  const itemRef = (index: number) => {\n    while (itemRefs.current.length <= index) {\n      itemRefs.current.push(React.createRef<HTMLButtonElement>())\n    }\n    return itemRefs.current[index]\n  }\n  const menuRefs = React.useRef<Array<React.RefObject<HTMLButtonElement | null>>>([])\n  const menuRef = (index: number) => {\n    while (menuRefs.current.length <= index) {\n      menuRefs.current.push(React.createRef<HTMLButtonElement>())\n    }\n    return menuRefs.current[index]\n  }\n\n  const visible = measured === null ? total : Math.min(measured, total)\n  const hidden = items.slice(visible)\n  const hasMore = hidden.length > 0\n  const menuOpen = open && hasMore\n\n  /**\n   * A primitive that changes when the set of controls does.\n   *\n   * The layout effect below cannot depend on `items` itself: almost every call site writes the array\n   * inline, so its identity changes on every render and the effect would force a layout every time.\n   * Width changes that leave the labels alone — a font swapping in, a `px-4` arriving from\n   * `itemClassName` — are caught by the resize observer on the probe instead, which is the only\n   * thing that can see them anyway.\n   */\n  const signature = items.map((item) => `${item.id ?? item.label}\\u0000${item.hideLabel ? 1 : 0}`).join(\"\\u001f\")\n\n  const measure = React.useCallback(() => {\n    // The row, not the outer box: the row is the one with a width the parent decides and an overflow\n    // that cannot be pushed outwards, which is what makes this measurement stable.\n    const row = rowRef.current\n    const probe = probeRef.current\n    if (!row || !probe) return\n    if (typeof getComputedStyle !== \"function\") return\n\n    const style = getComputedStyle(row)\n    // clientWidth is content plus padding, so a caller adding `px-3` would otherwise have its row\n    // measured against a box wider than the one the controls have to fit in.\n    const available =\n      row.clientWidth -\n      (parseFloat(style.paddingLeft) || 0) -\n      (parseFloat(style.paddingRight) || 0)\n    // A toolbar inside a closed tab, an unopened dialog or a collapsed accordion measures zero,\n    // which is not \"nothing fits\". Keeping the previous answer stops it folding to a lone trigger\n    // while off screen and staying that way once shown.\n    if (!(available > 0)) return\n\n    // Read from the row rather than taken as a prop, so `className=\"gap-2\"` is the single place the\n    // spacing is said. `normal` parses to NaN, which is zero gap here and is what a row with no gap\n    // utility actually renders.\n    const gap = parseFloat(style.columnGap) || 0\n\n    const probed = Array.from(probe.children)\n    if (probed.length === 0) return\n    const widths: number[] = []\n    for (let i = 0; i < probed.length - 1; i++) {\n      widths.push(probed[i].getBoundingClientRect().width)\n    }\n    const moreWidth = probed[probed.length - 1].getBoundingClientRect().width\n\n    setMeasured(fitToolbarItems(widths, available, gap, moreWidth))\n  }, [])\n\n  // Keyed on the signature so adding, removing or renaming a control re-measures; the two observers\n  // below cover every other reason a width changes.\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useIsomorphicLayoutEffect(measure, [measure, signature])\n\n  // The container getting narrower is the reason this component exists; the probe getting wider — a\n  // web font swapping in, a label arriving from a fetch — is the reason the same observer watches it\n  // too. A font swap changes every glyph without changing the container, so watching the row alone\n  // would leave the fold wherever the fallback font put it.\n  React.useEffect(() => {\n    const row = rowRef.current\n    const probe = probeRef.current\n    if (!row || typeof ResizeObserver === \"undefined\") return\n    const observer = new ResizeObserver(() => measure())\n    observer.observe(row)\n    if (probe) observer.observe(probe)\n    return () => observer.disconnect()\n  }, [measure])\n\n  const notifiedCount = React.useRef<number | null>(null)\n  React.useEffect(() => {\n    if (measured === null || notifiedCount.current === visible) return\n    notifiedCount.current = visible\n    onVisibleCountChange?.(visible)\n  }, [measured, visible, onVisibleCountChange])\n\n  // Reported from an effect rather than from inside the state setter: an updater has to be pure —\n  // React calls it twice in development's strict mode on purpose — so a caller placed in one is told\n  // twice for every open.\n  const notifiedOpen = React.useRef(false)\n  React.useEffect(() => {\n    if (notifiedOpen.current === open) return\n    notifiedOpen.current = open\n    onOpenChange?.(open)\n  }, [open, onOpenChange])\n\n  const closeMenu = React.useCallback((restoreFocus: boolean) => {\n    setMenuActive(-1)\n    setOpen(false)\n    if (restoreFocus) triggerRef.current?.focus()\n  }, [])\n\n  /**\n   * Whether focus is inside the **row** right now.\n   *\n   * The guard on moving focus during a rescue, and it asks about the row rather than the whole\n   * component for a second reason beyond the obvious one. The obvious one is that a window being\n   * resized is not a reason to take focus from whatever somebody is doing elsewhere on the page, so\n   * a toolbar that folds while nobody is in it moves its tab stop silently. The second is the open\n   * menu: focus there is inside the component but not in the row, and a re-measure that pulled it\n   * back to the trigger would close nothing, explain nothing, and drop the person out of the list\n   * they were reading. Asking the row answers both at once.\n   */\n  const holdsFocus = () => {\n    if (typeof document === \"undefined\") return false\n    const node = rowRef.current\n    const target = document.activeElement\n    return Boolean(node && target && node.contains(target))\n  }\n\n  // Moved on every measurement, focused only when focus was already here — the two halves of the\n  // rule, for the two different failures. See rescueToolbarStop.\n  React.useEffect(() => {\n    const rescued = rescueToolbarStop(active, visible, total)\n    if (rescued === active) return\n    const inside = holdsFocus()\n    setActive(rescued)\n    if (!inside) return\n    if (rescued === TOOLBAR_MORE) triggerRef.current?.focus()\n    else itemRef(rescued).current?.focus()\n    // itemRef mutates a ref and is stable; holdsFocus reads the document at call time.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [active, visible, total])\n\n  // Focus follows the active menu position, rather than `aria-activedescendant` pointing at it: the\n  // items of a menu *are* the interface, each one is a real `<button>`, and the practices for a menu\n  // describe moving focus.\n  //\n  // Bounded by the live count because `items` is a prop and the row re-measures underneath it, so\n  // the hidden set can shrink while the menu is open. The bound is about asking the right position\n  // rather than about crashing: the ref array is kept and reused across renders, so a position the\n  // menu no longer has may still be holding the node it had a render ago, and focusing that is a\n  // focus call into something nobody can see.\n  React.useEffect(() => {\n    if (!menuOpen || menuActive < 0 || menuActive >= hidden.length) return\n    menuRef(menuActive).current?.focus()\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [menuOpen, menuActive, hidden.length])\n\n  // The menu is given as a prop and the row re-measures underneath it, so the hidden set can shrink\n  // while it is open. Without this the active position points past the end and the focus effect asks\n  // a ref that holds nothing.\n  React.useEffect(() => {\n    if (!menuOpen) return\n    setMenuActive((current) => (current >= hidden.length ? hidden.length - 1 : current))\n  }, [menuOpen, hidden.length])\n\n  // A menu whose own trigger has gone — the row widened and everything fits — cannot be closed by\n  // the person looking at it.\n  React.useEffect(() => {\n    if (open && !hasMore) closeMenu(false)\n  }, [open, hasMore, closeMenu])\n\n  // Close on an outside pointer press, in the capture phase so it beats a focus move. Focus is not\n  // pulled back: the person has already said where they want to be.\n  React.useEffect(() => {\n    if (!menuOpen) return\n    function onPointerDown(event: PointerEvent) {\n      if (!rootRef.current?.contains(event.target as Node)) closeMenu(false)\n    }\n    document.addEventListener(\"pointerdown\", onPointerDown, true)\n    return () => document.removeEventListener(\"pointerdown\", onPointerDown, true)\n  }, [menuOpen, closeMenu])\n\n  function goTo(stop: number) {\n    setActive(stop)\n    if (stop === TOOLBAR_MORE) triggerRef.current?.focus()\n    else itemRef(stop).current?.focus()\n  }\n\n  function run(item: ToolbarOverflowItem) {\n    if (item.disabled) return\n    item.onSelect()\n  }\n\n  function chooseHidden(index: number) {\n    const item = hidden[index]\n    if (!item || item.disabled) return\n    // Closed before the handler runs, so the focus restore is not fighting a handler that opens a\n    // dialog of its own.\n    closeMenu(true)\n    item.onSelect()\n  }\n\n  function handleRowKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n    props.onKeyDown?.(event)\n    // The menu handles its own keys and prevents them; anything it prevented is not the row's.\n    if (event.defaultPrevented) return\n    // Asked of the row and not of the outer box. Direction is inherited, so the two usually agree —\n    // but `...props` land on the row, so a caller writing `dir=\"rtl\"` on this component sets it\n    // there, and the outer box above it would still answer with the page's direction.\n    const rtl = isRtl(rowRef.current)\n    switch (event.key) {\n      case \"ArrowRight\":\n        event.preventDefault()\n        goTo(nextToolbarStop(visible, hasMore, active, rtl ? -1 : 1))\n        break\n      case \"ArrowLeft\":\n        event.preventDefault()\n        goTo(nextToolbarStop(visible, hasMore, active, rtl ? 1 : -1))\n        break\n      case \"Home\":\n        event.preventDefault()\n        goTo(nextToolbarStop(visible, hasMore, 0, 0))\n        break\n      case \"End\":\n        event.preventDefault()\n        goTo(nextToolbarStop(visible, hasMore, TOOLBAR_MORE, 0))\n        break\n    }\n  }\n\n  function handleTriggerKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {\n    switch (event.key) {\n      case \"ArrowDown\":\n      case \"ArrowUp\":\n        // Prevented twice over: without it the page scrolls as well as the menu opening, and the\n        // row's own handler would read the same key as a move between controls.\n        event.preventDefault()\n        setOpen(true)\n        setMenuActive(nextMenuIndex(hidden.length, -1, event.key === \"ArrowDown\" ? 1 : -1))\n        break\n      case \"Escape\":\n        if (menuOpen) {\n          event.preventDefault()\n          closeMenu(true)\n        }\n        break\n    }\n  }\n\n  function handleMenuKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n    switch (event.key) {\n      case \"ArrowDown\":\n        event.preventDefault()\n        setMenuActive((current) => nextMenuIndex(hidden.length, current, 1))\n        break\n      case \"ArrowUp\":\n        event.preventDefault()\n        setMenuActive((current) => nextMenuIndex(hidden.length, current, -1))\n        break\n      case \"ArrowLeft\":\n      case \"ArrowRight\":\n        // Swallowed rather than acted on. A horizontal arrow means nothing in a vertical menu, and\n        // it must not reach the row — moving the toolbar's tab stop under an open menu leaves Escape\n        // returning focus to a control the person never left. The structure already prevents that,\n        // since the menu is a sibling of the row rather than inside it and nothing bubbles from one\n        // to the other; this is the second line, and it also stops a horizontally scrollable\n        // ancestor scrolling while somebody arrows down a menu.\n        event.preventDefault()\n        break\n      case \"Home\":\n        event.preventDefault()\n        setMenuActive(0)\n        break\n      case \"End\":\n        event.preventDefault()\n        setMenuActive(hidden.length - 1)\n        break\n      case \"Escape\":\n        event.preventDefault()\n        closeMenu(true)\n        break\n      case \"Tab\":\n        // Not prevented: Tab means leave, and the browser's answer for where to go is the right one.\n        // Focus is moved onto the trigger first so that answer is computed from an element still in\n        // the document — a Tab from a node that is being removed starts over at the top of the page.\n        closeMenu(true)\n        break\n    }\n  }\n\n  if (total === 0) return null\n\n  const control = (\n    item: ToolbarOverflowItem,\n    options: { index: number; probe: boolean }\n  ) => (\n    <button\n      key={`${item.id ?? item.label}-${options.probe ? \"probe\" : \"row\"}`}\n      ref={options.probe ? undefined : itemRef(options.index)}\n      type=\"button\"\n      // The probe is a measurement, not an interface: hidden from assistive technology, out of the\n      // tab order, and inert to a press. It is a `<button>` and not a `<span>` for the one reason\n      // that matters to a measurement — a UA stylesheet gives `button` its own font, padding and\n      // border, Tailwind's preflight normalises some of it and this component cannot assume\n      // preflight is there, and a width taken from the wrong element type is worse than none.\n      {...(options.probe ? { \"aria-hidden\": \"true\", tabIndex: -1 } : {})}\n      {...(options.probe\n        ? {}\n        : {\n            tabIndex: active === options.index ? 0 : -1,\n            \"aria-disabled\": item.disabled || undefined,\n            onFocus: () => setActive(options.index),\n            onClick: () => run(item),\n          })}\n      aria-label={item.hideLabel ? item.label : undefined}\n      // The browser's own tooltip, so an icon-only control says what it is to a mouse as well as to\n      // a screen reader. A styled tooltip is a different component.\n      title={item.hideLabel ? item.label : undefined}\n      className={cn(\n        CONTROL_BASE,\n        item.disabled ? \"pointer-events-none opacity-50\" : null,\n        itemClassName\n      )}\n    >\n      {item.icon ? (\n        <span aria-hidden=\"true\" className=\"flex size-4 items-center justify-center\">\n          {item.icon}\n        </span>\n      ) : null}\n      {item.hideLabel ? null : <span>{item.label}</span>}\n    </button>\n  )\n\n  const trigger = (probe: boolean) => (\n    <button\n      ref={probe ? undefined : triggerRef}\n      id={probe ? undefined : triggerId}\n      type=\"button\"\n      {...(probe\n        ? { \"aria-hidden\": \"true\", tabIndex: -1 }\n        : {\n            tabIndex: active === TOOLBAR_MORE ? 0 : -1,\n            \"aria-haspopup\": \"menu\" as const,\n            \"aria-expanded\": menuOpen,\n            \"aria-controls\": menuOpen ? menuId : undefined,\n            onFocus: () => setActive(TOOLBAR_MORE),\n            // Closing restores focus, as in `split-button`. A press has already put focus on the\n            // trigger, so it is usually a no-op — but a programmatic click with focus on a menu item\n            // would otherwise unmount the menu while it still holds focus, and focus drops to\n            // `<body>` from there.\n            onClick: () => (menuOpen ? closeMenu(true) : (setOpen(true), setMenuActive(0))),\n            onKeyDown: handleTriggerKeyDown,\n          })}\n      aria-label={labels.more(hidden.length, label)}\n      className={cn(CONTROL_BASE, \"px-2\", triggerClassName)}\n    >\n      <MoreHorizontal aria-hidden=\"true\" className=\"size-4\" />\n    </button>\n  )\n\n  return (\n    // The outer box exists for the menu, and only for the menu. `relative` makes it the menu's\n    // containing block; it deliberately does not clip, because the row below does — and an\n    // absolutely positioned panel hanging off the bottom of its own clipping containing block is\n    // painted only where it happens to overlap it. Measured in Chrome on this exact pair: with the\n    // menu inside the clipping box, a vertical scan of 15 points down the panel hit it 11 times and\n    // the 11 were exactly the slice inside the box; moved out to here, 15 of 15. A panel placed at\n    // `top-full` is entirely below the row, so inside it would have been painted nowhere at all.\n    <div ref={rootRef} className=\"relative w-full\">\n      <div\n        {...props}\n        ref={rowRef}\n        role=\"toolbar\"\n        aria-label={label}\n        // Explicit, though horizontal is the default for the role: this component only knows how to\n        // fold along one axis, and the attribute is what says so to a screen reader deciding which\n        // arrow keys to promise.\n        aria-orientation=\"horizontal\"\n        onKeyDown={handleRowKeyDown}\n        className={cn(\n          // `w-full` and `overflow-hidden` are the pair that makes the measurement stable: the width\n          // comes from the parent, and a row one pixel too wide is clipped rather than pushing the\n          // container wider — see the note on the component for the loop this removes. `relative`\n          // contains the probe, whose scrollable overflow this clipping then absorbs, so a toolbar\n          // far too narrow for its controls cannot make the page scroll sideways.\n          //\n          // The padding is load-bearing too, and it is this clipping that makes it so: a\n          // `focus-visible` ring is drawn outside the control's border box, and with the row flush\n          // against its controls the ring on the first and last of them is cut off. 4px clears a 2px\n          // ring on every side, and `measure` subtracts the row's padding, so the room costs nothing\n          // in the arithmetic. A caller setting `p-0` gets the clipped ring back.\n          \"relative flex w-full items-center gap-1 overflow-hidden p-1\",\n          className\n        )}\n      >\n        {items.slice(0, visible).map((item, index) => control(item, { index, probe: false }))}\n        {hasMore ? trigger(false) : null}\n\n        {/* The measuring copy: every control plus the trigger, at its natural width, never folded. */}\n        <div ref={probeRef} aria-hidden=\"true\" style={PROBE_STYLE}>\n          {items.map((item, index) => control(item, { index, probe: true }))}\n          {trigger(true)}\n        </div>\n      </div>\n\n      {menuOpen ? (\n        <div\n          id={menuId}\n          role=\"menu\"\n          // The attribute official shadcn/ui's collapsing components key their keyframes off, so\n          // `menuClassName=\"data-[state=open]:animate-in\"` works in a project that has installed any\n          // of them.\n          data-state=\"open\"\n          aria-labelledby={triggerId}\n          onKeyDown={handleMenuKeyDown}\n          className={cn(\n            \"absolute top-full z-50 mt-1 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md\",\n            align === \"end\" ? \"end-0\" : \"start-0\",\n            menuClassName\n          )}\n        >\n          {hidden.map((item, index) => (\n            <button\n              key={item.id ?? item.label}\n              ref={menuRef(index)}\n              role=\"menuitem\"\n              type=\"button\"\n              aria-disabled={item.disabled || undefined}\n              // Roving: one tab stop for the menu, on the item that has focus.\n              tabIndex={index === menuActive ? 0 : -1}\n              onClick={() => chooseHidden(index)}\n              className={cn(\n                \"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-start text-sm outline-none transition-colors\",\n                \"focus:bg-accent focus:text-accent-foreground\",\n                item.disabled\n                  ? \"pointer-events-none opacity-50\"\n                  : \"hover:bg-accent hover:text-accent-foreground\",\n                menuItemClassName\n              )}\n            >\n              {item.icon ? (\n                <span aria-hidden=\"true\" className=\"flex size-4 items-center justify-center\">\n                  {item.icon}\n                </span>\n              ) : null}\n              {/* The label is always shown here, whatever `hideLabel` says: a menu of bare icons is\n                  a menu of unlabelled squares. */}\n              <span className=\"truncate\">{item.label}</span>\n            </button>\n          ))}\n        </div>\n      ) : null}\n\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui",
  "docs": "Install any pulld component by name: add \"@pulld\": \"https://pulld.pages.dev/r/{name}.json\" to the registries block in components.json, then `npx shadcn add @pulld/<name>`. All 140 components: https://pulld.pages.dev/?utm_source=cli"
}
