{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "breadcrumb-overflow",
  "title": "Breadcrumb Overflow",
  "description": "A breadcrumb trail that measures itself and folds its middle into a menu when the row runs out of width, keeping the root you came from and the page you are on. Reach for it wherever a hierarchy has to survive a header nobody sized for it: an admin console's path, a file or storage browser, a category tree, a docs page's position, a deep settings screen, a project inside a workspace inside an organisation, a wizard's current step in a narrow panel, a dashboard that resizes with the window, a sidebar somebody can drag narrower, a trail that was two levels deep when it was designed and is five now. Common asks it answers: \"collapsing breadcrumb react\", \"breadcrumb overflow shadcn\", \"breadcrumb with ellipsis in the middle\", \"truncate breadcrumb middle\", \"collapse breadcrumb items when they do not fit\", \"responsive breadcrumb react\", \"breadcrumb too long narrow screen\", \"breadcrumb wraps to two lines\", \"stop breadcrumb from wrapping\", \"breadcrumb dropdown for hidden levels\", \"shadcn BreadcrumbEllipsis dropdown\", \"how many breadcrumb items fit in a container\", \"measure available width react\", \"ResizeObserver breadcrumb\", \"ResizeObserver loop completed with undelivered notifications\", \"items appear then disappear on load\", \"flicker when hiding overflowing elements\", \"measure the natural width of a hidden element\", \"MUI Breadcrumbs maxItems equivalent shadcn\", \"itemsBeforeCollapse itemsAfterCollapse\", \"antd breadcrumb overflow\", \"aria-current page breadcrumb\", \"breadcrumb accessibility current page\", \"is the current breadcrumb a link\", \"breadcrumb nav aria-label\", \"focus lost when an element unmounts\", \"focus jumps to body when the focused link is hidden\", \"keyboard access to collapsed breadcrumb items\", \"rtl breadcrumb chevron direction\", \"breadcrumb separator rtl\", \"dropdown clipped by overflow hidden\", \"absolutely positioned menu not visible inside overflow hidden\", \"パンくず 折りたたみ react\", \"パンくずが2行になる\", \"パンくず 省略 真ん中\", \"長いパンくず 幅に収める\", \"入りきらない階層をメニューに畳む\", \"パンくず ドロップダウン\", \"aria-current 現在地\", \"現在地はリンクにしない\", \"ResizeObserver 無限ループ\", \"測ってから隠すとちらつく\", \"overflow hidden でメニューが見えない\", \"RTL パンくず 矢印の向き\". What official shadcn/ui does instead is grow downwards, and that is a design decision rather than an omission: its `BreadcrumbList` is `flex flex-wrap`, so a trail that does not fit wraps onto another line. Measured on the same five-crumb path in Chrome today — one line at 480px, two lines and 45.2px tall at 320px, three lines and 70.8px at 240px, and zero horizontal overflow at every width. The header gets taller and everything below it moves down, which is fine for a page of prose and wrong for a toolbar row, a dialog title bar or a sticky header with a fixed height. The reflex fix is worse before it is better: `flex-nowrap` alone lets flex items shrink, and at 320px two of the five crumbs were squeezed until their own text wrapped — 39.2px tall against the 19.6px line — while the row still overflowed by 26px. So the crumbs here carry `shrink-0` and `whitespace-nowrap`, and something has to fold. Official has nothing that measures. Across its whole registry today — 65 items listed, 62 fetchable, `code-block`, `questionnaire` and `trace` indexed and 404 on both style tracks, 223,287 bytes of `files[].content` concatenated — `ResizeObserver`, `getBoundingClientRect`, `scrollWidth`, `offsetWidth` and `isTruncated` are every one of them a zero hit. Its `BreadcrumbEllipsis` is a static three-dot glyph you place yourself: `role=\"presentation\"`, `aria-hidden=\"true\"`, and an `sr-only` \"More\" **inside** that `aria-hidden` element, so the one label it carries is read by nothing, it is not a button, and nothing decides which levels it stands for. The two matches for `ellipsis` in the registry are that and `PaginationEllipsis`, and both are decoration. Measuring your own row is a loop unless you arrange otherwise. The implementation everybody writes first asks the live row how wide it is, hides what sticks out, and is asked again by its own ResizeObserver: the hidden crumbs now measure zero, so the trail fits, so they come back, so it overflows — 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 whole trail, absolutely positioned and `visibility: hidden` rather than `display: none` so it still has a box to measure, at `width: auto` and `max-width: none` so what it reports is the width each crumb wants. And the container's width cannot be changed by its contents: the list is a block-level, full-width box with `overflow-hidden`, so its width comes from its parent and a trail one pixel too wide is clipped rather than pushing anything wider. Together they make the fold a pure function of inputs that folding does not touch, so the same container width always produces the same answer and the observer settles after one pass. Because that answer is an object rather than a number, the bail-out React gives a state update to an equal value is kept by hand — the setter returns the previous object when both fields match, and without that one line every measurement is a render and the loop is back. The flash is a separate problem with a separate answer: the measurement runs in a layout effect, after layout and before paint, so a deep trail is never painted at full length and then collapsed. Which end gives way is the whole difference from every other overflow component. A toolbar keeps a prefix and folds the tail, because its first button is its most important one. A breadcrumb has to keep both ends — the root is how you get out and the current page is where you are — so what collapses is the middle, and the answer is how much of the tail survives with the root always in the row. Three pieces of arithmetic are easy to get wrong and each is written down in the source. A join between two crumbs costs `gap + separator + gap`, not one gap, because the list's gap applies on both sides of the separator: predicted against a real row, five crumbs came to 428.91px and measured 428.91px, and the folded shape predicted 211.05 and measured 211.05, while counting one gap per join under-reads a five-crumb trail by 24 pixels and keeps a level that does not fit. The ellipsis is charged for if and only if something folds — reserve room for it always and a trail that fits exactly loses a level to a control that was never needed; reserve it never and the level that \"fits\" is pushed out by the control added after the decision. And the fold never keeps `total - 1` trailing crumbs, which would be the whole trail behind a control hiding none of it, charging the row for itself and one more join. The menu cannot live in the list, and that is measured too. The list clips, which is what stops a trail far too narrow for its crumbs from making the page scroll sideways, and a panel absolutely positioned against the box that clips it is painted only where the two overlap: on this exact pair, a panel at `top-full` inside the clipping list was hit by 0 of 15 points scanned down its own rectangle, and the same panel moved onto the `<nav>` above it by 15 of 15. So the landmark positions the menu, the list clips, and the menu is placed under the ellipsis by arithmetic rather than by measuring it — the control is always the second box in the row, so its offset is the list's leading padding plus the root plus one join. Verified to the pixel in both writing directions: 68.25 predicted against 68.25 actual in LTR, 73.72 against 73.72 in RTL, with the panel landing on the control's inline edge in both. `inset-inline-start` counts from the inline start whichever way the text runs, so the sign flip that `scrollLeft` forces on an RTL page does not arise, and the default chevron mirrors itself rather than pointing back the way you came. The current page is not a link. It renders as a `<span aria-current=\"page\">` — that attribute is the whole convention for \"you are here\", and official's own page element instead gives a span `role=\"link\"` with `aria-disabled=\"true\"`, which announces a dimmed link for something that is not one and was never going anywhere. It is also the one crumb allowed to shrink, `min-w-0` with CSS truncation, so a trail with no room left shortens the longest thing in it instead of being cut off mid-glyph; the probe keeps reporting that crumb's natural width while the live one is squeezed — measured at 225.44px natural against 15.75px on screen — so the truncation cannot feed back into the arithmetic. Every other per-part class a caller passes is on the probe's copy as well as on the row, because the probe measures what the row renders: `separatorClassName=\"px-2\"` widens every join by four pixels, and a measurement that missed that would keep a level that does not fit. The keyboard is a breadcrumb's, not a toolbar's. The crumbs are links and each is an ordinary tab stop, because the practices give a breadcrumb no arrow keys and a roving tabindex here would take Tab's answer away from a reader. The ellipsis is the one control with keys of its own: Down or Up opens its menu on the first or last level, Escape closes it and returns focus, Tab leaves from an element still in the document, Home and End reach the ends, a horizontal arrow inside the menu is swallowed rather than left to scroll an ancestor, and the wrap is `split-button`'s own `nextMenuIndex`, imported rather than written again so the two menus in this registry cannot drift apart. One failure survives all of that and is handled on its own: a crumb holding focus can be folded away when the container narrows, and a browser does not move focus when the element holding it leaves the document — it drops to `<body>`, so the next Tab starts from the top of the page. The fold moves focus to the ellipsis, which is where that crumb now is, and only when focus was already in the trail, since a window being dragged narrower is not a reason to take focus from whatever somebody is doing elsewhere on the page. What you get: `BreadcrumbOverflow`, taking `items` root-first with the current page last, each `{ label, href?, icon?, id?, onNavigate? }` — `href` omitted for a level with no page of its own, which is text in the row and still a focusable rung in the menu, and `onNavigate` the hook for a client router (`event.preventDefault()` then `router.push`), which is also what tells the component the page is not being replaced so it puts focus back on the control instead of leaving it on a link that is about to unmount. `label` names the landmark (\"Breadcrumb\" by default), `separator` replaces the chevron in the row and in the probe alike, `labels.more(count)` builds the control's accessible name and carries the number of hidden levels because \"More\" alone does not say whether one level is hidden or nine, `onFoldChange` reports how many are behind the control after each measurement, and `onOpenChange` is told once per change rather than twice under strict mode. `fitBreadcrumbs` is the whole decision as a plain function of widths, available width, gap, separator and control width — the part worth asserting, since every interesting case here is a width rather than a click — and `breadcrumbMenuOffset` and `isCrumbFolded` are the placement and the fold membership, each on its own. Per-part `className` for the list, the crumbs, the current page, the separators, the control, the panel and its items. One file, two lucide icons, no `radix-ui`, and nothing portalled or measured against the viewport: the honest limits are the server, which cannot measure, so an SSR'd page shows the whole trail clipped until hydration, and an ancestor that scrolls, which can clip the panel. Distinct from its neighbours here: `toolbar-overflow` folds a row of actions into a menu at its end, keeping a prefix rather than both ends, and this borrows its probe discipline rather than its strategy; `middle-truncate` shortens one string from the middle so an extension survives, where this decides which whole levels survive; `overflow-tooltip` reveals a string that CSS has already clipped, which is the answer for the current page's title rather than for the trail; `scroll-shadow` says there is more past a scrolling edge instead of folding it; `sort-header`, `column-resizer` and `sticky-table-header` measure a table, while this measures itself; `split-button` is one action with its variants under an arrow, and the menu here composes with it.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://pulld.pages.dev/r/split-button.json"
  ],
  "files": [
    {
      "path": "registry/ui/breadcrumb-overflow.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRight, 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 `toolbar-overflow` and `middle-truncate` allow themselves\n * and for the same reason: element widths are fractional, so a trail 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 a level that would have fitted, and every folded level is one more click\n * between somebody and where they came from.\n */\nconst WIDTH_TOLERANCE = 0.5\n\n/**\n * What one join between two boxes costs.\n *\n * A breadcrumb is not a plain row: between two crumbs sits a separator, and the list's `gap` applies\n * on both sides of it, so every join is `gap + separator + gap` rather than one gap. Measured in\n * Chrome against the real markup — five crumbs at their natural widths, `column-gap: 6px`, a 14px\n * chevron — where the predicted extent `sum(boxes) + (boxes - 1) * (separator + 2 * gap)` came to\n * 428.91px and the row measured 428.91px; the folded shape (root, trigger, current page) predicted\n * 211.05 and measured 211.05. Counting one gap per join instead of two is the quiet version of the\n * mistake: it under-reads a five-crumb trail by 24 pixels and keeps a level that does not fit.\n */\nfunction joinWidth(gap: number, separatorWidth: number): number {\n  return size(separatorWidth) + 2 * size(gap)\n}\n\n/** A measurement that cannot be trusted — detached, `display: none`, NaN from a `normal` gap — is 0. */\nfunction size(value: number): number {\n  return Number.isFinite(value) && value > 0 ? value : 0\n}\n\n/**\n * How many crumbs at the **end** of the trail stay in the row.\n *\n * This is the whole decision, as arithmetic over numbers nothing in the DOM can argue with, and it\n * is exported because it is the part worth asserting: every interesting case here is a width rather\n * than a click.\n *\n * `widths` are the crumbs' **natural** widths in trail order, root first and the current page last.\n * `available` is the list's content width, `gap` its column gap, `separatorWidth` one separator and\n * `triggerWidth` the ellipsis control that stands in for whatever folds.\n *\n * The return value is a count of trailing crumbs, and `widths.length` is the answer that means\n * nothing folded. **Where this differs from every other overflow component is which end gives way.**\n * A toolbar keeps a prefix and folds the tail, because the first button is the most important one. A\n * breadcrumb has to keep *both* ends — the root is how you get out and the current page is where you\n * are — so what collapses is the middle, and the answer is \"how much of the tail survives\" with the\n * root always in the row. Reusing a prefix strategy here produces a trail that reads\n * \"Home / Workspaces / Acme Corp / …\", which has thrown away the one crumb nobody can navigate\n * without.\n *\n * Two bounds are load-bearing. The floor is one: the current page is never folded, so a trail with\n * no room at all still shows where you are and the row degrades by shortening that crumb's text\n * rather than by hiding it. The ceiling is `total - 2`, because keeping `total - 1` trailing crumbs\n * plus the root is every crumb there is — the ellipsis would then be a control that hides nothing,\n * while still charging the row for itself and for one more join.\n *\n * The trigger's own width is charged if and only if something folds, the same rule\n * `fitToolbarItems` arrives at from the other direction: reserve room for it always and a trail that\n * fits exactly loses a level to a control that was never needed; reserve it never and the level that\n * \"fits\" is pushed out by the control added after the decision.\n */\nexport function fitBreadcrumbs(\n  widths: readonly number[],\n  available: number,\n  gap: number,\n  separatorWidth: number,\n  triggerWidth: number\n): number {\n  const total = widths.length\n  // Two crumbs have no middle. There is nothing this component is willing to fold, so it says so\n  // rather than folding the root or the current page, and a narrow container clips or truncates.\n  if (total <= 2) return total\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 sizes = widths.map(size)\n  const join = joinWidth(gap, separatorWidth)\n\n  let full = (total - 1) * join\n  for (const width of sizes) full += width\n  if (full - room <= WIDTH_TOLERANCE) return total\n\n  // Past here the middle folds, so the row carries the root, the trigger and a tail of crumbs: that\n  // is `kept + 2` boxes and `kept + 1` joins.\n  const trigger = size(triggerWidth)\n  let used = sizes[0] + trigger + sizes[total - 1] + 2 * join\n  let kept = 1\n  for (let k = 2; k <= total - 2; k++) {\n    const next = used + join + sizes[total - k]\n    if (next - room > WIDTH_TOLERANCE) break\n    used = next\n    kept = k\n  }\n  return kept\n}\n\n/**\n * Where the folded-levels menu sits, as a distance from the list's inline start.\n *\n * The menu hangs off the trail's outer box rather than off the control that opens it — see\n * {@link BreadcrumbOverflow} for why it cannot live inside the list — so something has to say where\n * the control is. Nothing needs to be measured for it: the ellipsis is always the second box in the\n * row, so its offset is the list's leading padding plus the root crumb plus exactly one join, and\n * every one of those numbers is already in hand from the probe.\n *\n * Checked in Chrome against the live control in both writing directions: predicted 68.25px against\n * an actual 68.25px in LTR, and 73.72 against 73.72 in RTL, with a panel given that offset through\n * `inset-inline-start` landing on the control's inline edge to the pixel in both. Logical insets\n * count from the inline start whichever way the text runs, so the sign flip that `scroll-shadow`\n * has to handle for `scrollLeft` does not arise here.\n */\nexport function breadcrumbMenuOffset(\n  rootWidth: number,\n  gap: number,\n  separatorWidth: number,\n  paddingStart: number\n): number {\n  return size(paddingStart) + size(rootWidth) + joinWidth(gap, separatorWidth)\n}\n\n/**\n * Whether the crumb at `index` is one of the folded ones, given how many trailing crumbs are kept.\n *\n * The rescue below is the only caller, and it is a separate function because \"is this crumb in the\n * menu\" is asked of a position that was recorded a render ago — the kind of off-by-one that is\n * invisible until a focus call goes to the wrong element.\n */\nexport function isCrumbFolded(index: number, kept: number, total: number): boolean {\n  if (!Number.isFinite(index) || !Number.isFinite(kept) || kept >= total) return false\n  return index > 0 && index < total - kept\n}\n\nexport interface BreadcrumbCrumb {\n  /** Stable key. The label is used when this is absent. */\n  id?: string\n  /** The crumb's text, and its accessible name. */\n  label: string\n  /**\n   * Where the crumb goes. Omitted for a level that exists in the hierarchy but has no page of its\n   * own, which renders as plain text in the row and in the menu.\n   *\n   * Ignored on the last crumb: that one is the page you are already on.\n   */\n  href?: string\n  /** Decorative icon before the label — a house on the root, a file glyph on a leaf. Hidden from screen readers. */\n  icon?: React.ReactNode\n  /**\n   * Called when the crumb is activated, before the browser follows the link.\n   *\n   * The hook for a client router: `onNavigate={(event) => { event.preventDefault(); router.push(href) }}`.\n   * Preventing the default is also what tells this component the page is not being replaced, so it\n   * puts focus back on the ellipsis control instead of leaving it on a link that is about to unmount.\n   */\n  onNavigate?: (event: React.MouseEvent<HTMLAnchorElement>) => void\n}\n\nexport interface BreadcrumbOverflowLabels {\n  /**\n   * The ellipsis control's accessible name, from the number of levels behind it.\n   *\n   * A function rather than a string because the count is the one piece of information the control\n   * has that nothing else on screen does — official shadcn/ui's own ellipsis carries the word\n   * \"More\" and no number, and \"More\" alone does not say whether one level is hidden or nine.\n   */\n  more: (count: number) => string\n}\n\nexport const defaultBreadcrumbOverflowLabels: BreadcrumbOverflowLabels = {\n  more: (count) => (count === 1 ? \"Show 1 hidden level\" : `Show ${count} hidden levels`),\n}\n\nconst CRUMB_BASE =\n  \"inline-flex items-center gap-1.5 rounded-sm transition-colors focus-visible:outline-none \" +\n  \"focus-visible:ring-2 focus-visible:ring-ring\"\n\n/**\n * The probe takes part in no layout: absolutely positioned, so it adds nothing to the row's width,\n * and `visibility: hidden` rather than `display: none` because a `display: none` box reports zero\n * and the point of it is to be measurable. `width: auto` and `max-width: none` are what make the\n * numbers it reports the width each crumb *wants* rather than the width it was given.\n */\nconst PROBE_STYLE: React.CSSProperties = {\n  position: \"absolute\",\n  top: 0,\n  insetInlineStart: 0,\n  display: \"flex\",\n  alignItems: \"center\",\n  visibility: \"hidden\",\n  pointerEvents: \"none\",\n  whiteSpace: \"nowrap\",\n  width: \"auto\",\n  maxWidth: \"none\",\n  userSelect: \"none\",\n}\n\ninterface Layout {\n  /** How many trailing crumbs are in the row. */\n  kept: number\n  /** Where the menu goes, from the list's inline start. */\n  offset: number\n}\n\nexport interface BreadcrumbOverflowProps\n  extends Omit<React.ComponentPropsWithoutRef<\"nav\">, \"children\" | \"aria-label\"> {\n  /** The trail, root first and the current page last. */\n  items: readonly BreadcrumbCrumb[]\n  /**\n   * The navigation landmark's accessible name. \"Breadcrumb\" is the convention and the default; a\n   * page with two trails should name them apart.\n   */\n  label?: string\n  /** What goes between two crumbs. A chevron by default, mirrored on an RTL page. */\n  separator?: React.ReactNode\n  /** Told when the folded-levels menu opens and closes. */\n  onOpenChange?: (open: boolean) => void\n  /**\n   * Told how many levels are behind the ellipsis after each measurement — 0 when the whole trail is\n   * in the row. For a caller that wants to log how often its hierarchy is too deep for the header.\n   */\n  onFoldChange?: (hidden: number) => void\n  labels?: Partial<BreadcrumbOverflowLabels>\n  /**\n   * On the `<ol>` — the trail itself, which is what anybody looking at the page would call the\n   * breadcrumb, and the element whose `gap` the arithmetic reads back. The `<nav>` above it is a\n   * positioning wrapper that exists so the menu is not clipped; `...props` reach it, since the\n   * landmark is the thing a caller means by \"the breadcrumb\".\n   */\n  className?: string\n  itemClassName?: string\n  currentClassName?: string\n  separatorClassName?: string\n  triggerClassName?: string\n  menuClassName?: string\n  menuItemClassName?: string\n}\n\n// Measures before paint, so a deep trail is never briefly on screen at full length. `useEffect` on\n// the server, where there is nothing to measure and `useLayoutEffect` warns.\nconst useIsomorphicLayoutEffect =\n  typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect\n\n/**\n * A breadcrumb trail that folds its **middle** into a menu when the row runs out of width, keeping\n * the root you came from and the page you are on.\n *\n * For an admin console's hierarchy, a file or storage browser, a category path, a docs sidebar's\n * current position, a deep settings page, anywhere a trail has to survive a width nobody chose.\n *\n * **What official shadcn/ui does instead is grow downwards.** Its `BreadcrumbList` is\n * `flex flex-wrap`, so a trail that does not fit wraps: measured on the same five-crumb path, one\n * line at 480px, two lines and 45.2px tall at 320px, three lines and 70.8px at 240px, with zero\n * horizontal overflow at every width — the header gets taller and the content below it moves down.\n * The reflex fix of `flex-nowrap` is worse before it is better: flex items shrink by default, so at\n * 320px two of the five crumbs were squeezed until their own text wrapped (39.2px tall against the\n * 19.6px line) *and* the row still overflowed by 26px. Which is why the crumbs here carry `shrink-0`\n * and `whitespace-nowrap`, and why something has to fold.\n *\n * **Measuring your own row is a loop unless you arrange otherwise.** The naive implementation asks\n * the live row how wide it is, hides what sticks out, and is then asked again by its own\n * ResizeObserver: the hidden crumbs now measure zero, so the trail fits, so they come back. Two\n * things break that here, both borrowed from `toolbar-overflow`:\n *\n *   - **Natural widths come from a probe that is never folded.** A hidden copy of the whole trail\n *     sits in the list, out of flow, and is what gets measured. Folding the visible row cannot\n *     change what the probe says.\n *   - **The container's width cannot be changed by its contents.** The list is a block-level,\n *     full-width box, so its width comes from its parent.\n *\n * Together they make the fold a pure function of inputs that folding does not touch — see\n * {@link fitBreadcrumbs} — so the same container width always produces the same answer and the\n * observer settles after one pass. Because that answer is an object here rather than a number, the\n * bail-out React gives a state update to an equal value has to be kept by hand: the setter returns\n * the previous object when both fields match, and without that line every measurement is a\n * re-render and the loop is back.\n *\n * **The menu cannot live in the list.** The list clips — that is what keeps a trail far too narrow\n * for its crumbs from making the page scroll sideways — and a panel absolutely positioned against\n * the box clipping it is painted only where the two overlap. Measured on this exact pair: a panel at\n * `top-full` inside the clipping list was hit by **0 of 15** points scanned down its own rectangle,\n * and the same panel moved onto the `<nav>` above it by **15 of 15**. So the landmark positions the\n * menu, the list clips, and the menu is placed under the ellipsis by arithmetic rather than by\n * measuring it — {@link breadcrumbMenuOffset}, verified to the pixel in both writing directions.\n *\n * **The current page is not a link.** It renders as a `<span aria-current=\"page\">`: that attribute\n * is the whole convention for \"you are here\", and it is also the one crumb that is allowed to shrink\n * — `min-w-0` with CSS truncation — so a trail with no room left shortens the longest thing in it\n * instead of being cut off mid-glyph. The probe keeps reporting that crumb's natural width while the\n * live one is squeezed (measured: 225.44px natural against 15.75px on screen), so the arithmetic is\n * untouched by the truncation.\n *\n * The keyboard is a breadcrumb's, not a toolbar's: the crumbs are links and each is an ordinary tab\n * stop, because the practices give a breadcrumb no arrow keys and a roving tabindex here would take\n * Tab's answer away from a reader. The ellipsis is the one control with keys of its own — Down or Up\n * opens its menu, Escape closes it and returns focus, Tab leaves, and inside the menu Up and Down\n * wrap through the folded levels using `split-button`'s own `nextMenuIndex`, imported rather than\n * written again so the two menus in this registry cannot drift apart.\n *\n * One failure survives all of that and is handled on its own: a crumb holding focus can be folded\n * away when the container narrows, and a browser does not move focus when the element holding it\n * leaves the document — it drops to `<body>`, so the next Tab starts from the top of the page. So\n * the fold moves focus to the ellipsis, which is where that crumb now is, and only when focus was\n * already in the trail: a window being dragged narrower is not a reason to take focus from whatever\n * somebody is doing elsewhere on the page.\n *\n * The honest limit is the server, which cannot measure: an SSR'd page shows the whole trail, clipped\n * by the list, until hydration. Nothing is portalled and nothing measures the viewport, so a menu\n * near the edge of a narrow page can be cut by an ancestor that scrolls — put the scroll container\n * somewhere it is allowed to escape.\n */\nexport function BreadcrumbOverflow({\n  items,\n  label = \"Breadcrumb\",\n  separator,\n  onOpenChange,\n  onFoldChange,\n  labels: labelOverrides,\n  className,\n  itemClassName,\n  currentClassName,\n  separatorClassName,\n  triggerClassName,\n  menuClassName,\n  menuItemClassName,\n  ...props\n}: BreadcrumbOverflowProps) {\n  const labels = React.useMemo(\n    () => ({ ...defaultBreadcrumbOverflowLabels, ...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 trail\n  // and the layout effect below folds it before paint.\n  const [layout, setLayout] = React.useState<Layout | null>(null)\n  const [open, setOpen] = React.useState(false)\n  // Which folded level 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 BreadcrumbOverflow}. `navRef` is the\n  // landmark the menu hangs off; `listRef` is the trail, and the only one that clips.\n  const navRef = React.useRef<HTMLElement | null>(null)\n  const listRef = React.useRef<HTMLOListElement | null>(null)\n  const probeRef = React.useRef<HTMLLIElement | null>(null)\n  const triggerRef = React.useRef<HTMLButtonElement | null>(null)\n\n  /**\n   * The crumb that last took focus, as a position, or null.\n   *\n   * A ref and not state: it changes on every focus and blur, and nothing rendered depends on it —\n   * the rescue below reads it at the moment the fold changes.\n   */\n  const focusedCrumb = React.useRef<number | null>(null)\n\n  /**\n   * One ref object per folded position, created lazily — object refs rather than callback refs for\n   * the 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.\n   */\n  const menuRefs = React.useRef<Array<React.RefObject<HTMLAnchorElement | null>>>([])\n  const menuRef = (index: number) => {\n    while (menuRefs.current.length <= index) {\n      menuRefs.current.push(React.createRef<HTMLAnchorElement>())\n    }\n    return menuRefs.current[index]\n  }\n\n  const kept = layout === null ? total : Math.min(layout.kept, total)\n  const folded = kept >= total ? [] : items.slice(1, total - kept)\n  const hasMore = folded.length > 0\n  const menuOpen = open && hasMore\n\n  /**\n   * A primitive that changes when the trail does.\n   *\n   * The layout effect 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-2` 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).join(\"\\u001f\")\n\n  const measure = React.useCallback(() => {\n    // The list, not the landmark: the list is the one with a width its parent decides and an\n    // overflow that cannot be pushed outwards, which is what makes this measurement stable.\n    const list = listRef.current\n    const probe = probeRef.current\n    if (!list || !probe) return\n    if (typeof getComputedStyle !== \"function\") return\n\n    const style = getComputedStyle(list)\n    // The *inline* start, which is the right-hand side on an RTL page: the menu's offset is counted\n    // from there, and reading `paddingLeft` would place it a padding's width into the wrong end.\n    const rtl = style.direction === \"rtl\"\n    const paddingStart = parseFloat(rtl ? style.paddingRight : style.paddingLeft) || 0\n    const paddingEnd = parseFloat(rtl ? style.paddingLeft : style.paddingRight) || 0\n    // clientWidth is content plus padding, so a caller adding `px-3` would otherwise have its trail\n    // measured against a box wider than the one the crumbs have to fit in.\n    const available = list.clientWidth - paddingStart - paddingEnd\n    // A breadcrumb inside a closed tab, an unopened dialog or a collapsed panel measures zero, which\n    // is not \"nothing fits\". Keeping the previous answer stops it folding to a lone ellipsis while\n    // off screen and staying that way once shown.\n    if (!(available > 0)) return\n\n    // Read from the list 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 list with no gap\n    // utility actually renders.\n    const gap = parseFloat(style.columnGap) || 0\n\n    // The probe's children, in the order it renders them: every crumb, then one separator, then the\n    // ellipsis. Short of that is a probe mid-commit, and measuring it would fold against widths that\n    // belong to a different trail.\n    const probed = Array.from(probe.children)\n    if (probed.length < total + 2) return\n    const widths = probed.slice(0, total).map((node) => node.getBoundingClientRect().width)\n    const separatorWidth = probed[total].getBoundingClientRect().width\n    const triggerWidth = probed[total + 1].getBoundingClientRect().width\n\n    const next: Layout = {\n      kept: fitBreadcrumbs(widths, available, gap, separatorWidth, triggerWidth),\n      offset: breadcrumbMenuOffset(widths[0], gap, separatorWidth, paddingStart),\n    }\n    // The bail-out React gives an equal value, kept by hand because this one is an object: without\n    // it every measurement is a new identity, every new identity is a render, and the observer that\n    // watches the result of the render fires again.\n    setLayout((prev) =>\n      prev && prev.kept === next.kept && prev.offset === next.offset ? prev : next\n    )\n  }, [total])\n\n  // Keyed on the signature so adding, removing or renaming a crumb re-measures; the observer below\n  // covers 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. `overflow-tooltip` measured that one: a box sized for the fallback face reported that its\n  // text fitted and the same box was 8px over once the real face arrived, with no resize to notice.\n  React.useEffect(() => {\n    const list = listRef.current\n    const probe = probeRef.current\n    if (!list || typeof ResizeObserver === \"undefined\") return\n    const observer = new ResizeObserver(() => measure())\n    observer.observe(list)\n    if (probe) observer.observe(probe)\n    return () => observer.disconnect()\n  }, [measure])\n\n  const notifiedFold = React.useRef<number | null>(null)\n  React.useEffect(() => {\n    if (layout === null || notifiedFold.current === folded.length) return\n    notifiedFold.current = folded.length\n    onFoldChange?.(folded.length)\n  }, [layout, folded.length, onFoldChange])\n\n  // Reported from an effect rather than from inside a 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 **list** right now.\n   *\n   * The guard on the rescue, and it asks about the list rather than the whole component for two\n   * reasons. A window being resized is not a reason to take focus from whatever somebody is doing\n   * elsewhere on the page. And focus in the open menu is inside the component but not in the list, so\n   * a re-measure that pulled it back to the ellipsis would close nothing, explain nothing, and drop\n   * the person out of the list of levels they were reading.\n   */\n  const holdsFocus = () => {\n    if (typeof document === \"undefined\") return false\n    const node = listRef.current\n    const target = document.activeElement\n    return Boolean(node && target && node.contains(target))\n  }\n\n  // A crumb folded while it holds focus. The position is cleared as it is used: the link is gone, so\n  // there is nothing left to rescue and a second measurement must not fire this again.\n  React.useEffect(() => {\n    const index = focusedCrumb.current\n    if (index === null || !isCrumbFolded(index, kept, total)) return\n    if (!holdsFocus()) return\n    focusedCrumb.current = null\n    triggerRef.current?.focus()\n    // holdsFocus reads the document at call time; the ref is stable.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [kept, 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 link, and the practices for a menu\n  // describe moving focus.\n  //\n  // Bounded by the live count because `items` is a prop and the trail re-measures underneath it, so\n  // the folded set can shrink while the menu is open. The ref array is kept across renders, so a\n  // position the menu no longer has may still hold the node it had a render ago, and focusing that is\n  // a focus call into something nobody can see.\n  React.useEffect(() => {\n    if (!menuOpen || menuActive < 0 || menuActive >= folded.length) return\n    menuRef(menuActive).current?.focus()\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [menuOpen, menuActive, folded.length])\n\n  // The container widened while the menu was open, so there are fewer levels behind it than the\n  // active position names. Without this the position points past the end and the focus effect asks a\n  // ref that holds nothing.\n  React.useEffect(() => {\n    if (!menuOpen) return\n    setMenuActive((current) => (current >= folded.length ? folded.length - 1 : current))\n  }, [menuOpen, folded.length])\n\n  // A menu whose own control has gone — the trail 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 (!navRef.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 handleTriggerKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {\n    switch (event.key) {\n      case \"ArrowDown\":\n      case \"ArrowUp\":\n        // Prevented so the page does not scroll as well as the menu opening.\n        event.preventDefault()\n        setOpen(true)\n        setMenuActive(nextMenuIndex(folded.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(folded.length, current, 1))\n        break\n      case \"ArrowUp\":\n        event.preventDefault()\n        setMenuActive((current) => nextMenuIndex(folded.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        // letting it through would scroll a horizontally scrollable ancestor underneath somebody\n        // reading the 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(folded.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 control first so that answer is computed from an element still in\n        // the document — a Tab from a node being removed starts over at the top of the page.\n        closeMenu(true)\n        break\n    }\n  }\n\n  function chooseFolded(index: number, event: React.MouseEvent<HTMLAnchorElement>) {\n    const crumb = folded[index]\n    if (!crumb) return\n    crumb.onNavigate?.(event)\n    // Focus goes back to the control only when the page is not being replaced. A client router says\n    // so by preventing the default; a real navigation takes focus with it, and reaching for the\n    // control on the way out would be a focus call into a document that is leaving.\n    closeMenu(event.defaultPrevented)\n  }\n\n  if (total === 0) return null\n\n  const crumbContent = (crumb: BreadcrumbCrumb) => (\n    <>\n      {crumb.icon ? (\n        <span aria-hidden=\"true\" className=\"flex size-4 shrink-0 items-center justify-center\">\n          {crumb.icon}\n        </span>\n      ) : null}\n      {crumb.label}\n    </>\n  )\n\n  const crumbItem = (\n    crumb: BreadcrumbCrumb,\n    options: { index: number; probe: boolean; current: boolean }\n  ) => {\n    const { index, probe, current } = options\n    // The current page, which is a `<span aria-current=\"page\">` and never a link: it is where you\n    // already are, so there is nothing to follow, and `aria-current` is the whole convention for\n    // saying so. In the row it is also the one crumb allowed to shrink, which is what turns a trail\n    // with no room left into a shortened title rather than a cut-off one — `min-w-0` because a flex\n    // item will not shrink below its content without it. The probe copy never truncates: its job is\n    // to report the natural width, and it is the width the arithmetic runs on.\n    if (current) {\n      return (\n        <span\n          // Marked hidden on the copy itself and not only through the probe's own `aria-hidden`,\n          // which would already hide the whole subtree. Saying it on the node is what makes \"is this\n          // the crumb somebody can see\" answerable from the node — the ancestor-walking version is\n          // the mistake official's ellipsis makes from the other side, where an `sr-only` label\n          // sits inside an `aria-hidden` element and is read by nothing.\n          aria-hidden={probe ? \"true\" : undefined}\n          aria-current={probe ? undefined : \"page\"}\n          className={cn(\n            CRUMB_BASE,\n            \"font-normal text-foreground\",\n            probe ? \"shrink-0 whitespace-nowrap\" : \"min-w-0 truncate\",\n            itemClassName,\n            currentClassName\n          )}\n        >\n          {crumbContent(crumb)}\n        </span>\n      )\n    }\n    // No `href` on the probe copy, which is what keeps it out of the focus order entirely rather\n    // than merely out of the tab sequence — an `aria-hidden` element that can still be focused is\n    // the one combination assistive technology cannot explain.\n    const Tag = crumb.href && !probe ? \"a\" : \"span\"\n    return (\n      <Tag\n        {...(probe\n          ? { \"aria-hidden\": \"true\" as const }\n          : {\n              onFocus: () => {\n                focusedCrumb.current = index\n              },\n              onBlur: () => {\n                if (focusedCrumb.current === index) focusedCrumb.current = null\n              },\n            })}\n        {...(Tag === \"a\"\n          ? {\n              href: crumb.href,\n              onClick: (event: React.MouseEvent<HTMLAnchorElement>) => crumb.onNavigate?.(event),\n            }\n          : {})}\n        className={cn(\n          CRUMB_BASE,\n          \"shrink-0 whitespace-nowrap\",\n          crumb.href && !probe ? \"hover:text-foreground\" : null,\n          itemClassName\n        )}\n      >\n        {crumbContent(crumb)}\n      </Tag>\n    )\n  }\n\n  const separatorNode = (key?: string) => (\n    <li\n      key={key}\n      // `role=\"presentation\"` and `aria-hidden` together, the way official shadcn/ui's separator\n      // does it: the chevron is punctuation, and a screen reader reading \"greater than\" between\n      // every level is the noise this removes.\n      role=\"presentation\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"flex shrink-0 items-center [&>svg]:size-3.5\",\n        // The trail runs the other way on an RTL page, so the chevron has to as well — official's\n        // separator does not do this, and a right-pointing chevron in a right-to-left trail points\n        // back the way you came.\n        \"rtl:[&>svg]:-scale-x-100\",\n        // The caller's class, on the copy as well as on the row — which is why this takes no\n        // `probe` flag: the two separators are identical, and the probe's one is a separator too.\n        // A caller's `separatorClassName=\"px-2\"` widens every\n        // join by four pixels, and a probe that measures the unstyled separator under-reads the\n        // trail by that much per level — which is a level kept that does not fit. The rule is the\n        // same for the control below: **the probe measures what the row renders**, and the only\n        // differences allowed are the ones that make it a measurement rather than an interface.\n        separatorClassName\n      )}\n    >\n      {separator ?? <ChevronRight />}\n    </li>\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\" as const, tabIndex: -1 }\n        : {\n            \"aria-haspopup\": \"menu\" as const,\n            \"aria-expanded\": menuOpen,\n            \"aria-controls\": menuOpen ? menuId : undefined,\n            // Closing restores focus, as in `split-button`. A press has already put focus here, so\n            // it is usually a no-op — but a programmatic click with focus on a menu item would\n            // otherwise unmount the menu while it still holds focus, and focus drops to `<body>`.\n            onClick: () => (menuOpen ? closeMenu(true) : (setOpen(true), setMenuActive(0))),\n            onKeyDown: handleTriggerKeyDown,\n          })}\n      // The name carries the count, which official's ellipsis cannot: its `sr-only` \"More\" sits\n      // inside an `aria-hidden` span, so assistive technology is told nothing at all.\n      aria-label={labels.more(folded.length)}\n      className={cn(\n        \"flex size-6 shrink-0 items-center justify-center rounded-sm transition-colors\",\n        \"hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n        // On the copy too: `triggerClassName=\"size-9\"` is a 12px difference between the control the\n        // arithmetic charges for and the one in the row, and it is charged exactly when the row is\n        // already out of space.\n        triggerClassName\n      )}\n    >\n      <MoreHorizontal aria-hidden=\"true\" className=\"size-4\" />\n    </button>\n  )\n\n  /**\n   * The crumbs in the row, each carrying **its own position in the trail** rather than its position\n   * in the row.\n   *\n   * The pair is kept together on purpose. The two numbers agree only while nothing is folded, and\n   * deriving one from the other is where this went wrong first: arithmetic that is right for the\n   * folded row is off by one for the whole trail, which takes `aria-current` off the current page —\n   * the single attribute the pattern exists to carry — on exactly the trails that are wide enough\n   * not to fold. `hasMore` is the discriminator because it is also what decides whether there is a\n   * trigger in the row at all.\n   */\n  const visible: Array<{ crumb: BreadcrumbCrumb; index: number }> = hasMore\n    ? [\n        { crumb: items[0], index: 0 },\n        ...items.slice(total - kept).map((crumb, offset) => ({\n          crumb,\n          index: total - kept + offset,\n        })),\n      ]\n    : items.map((crumb, index) => ({ crumb, index }))\n\n  const row: React.ReactNode[] = []\n  visible.forEach(({ crumb, index }, position) => {\n    const current = index === total - 1\n    if (position > 0) {\n      row.push(separatorNode(`sep-${index}`))\n      // The ellipsis stands where the folded levels were: after the root's separator, before the\n      // surviving tail. Its position is why {@link breadcrumbMenuOffset} needs nothing measured.\n      if (position === 1 && hasMore) {\n        row.push(\n          <li key=\"more\" className=\"flex shrink-0 items-center\">\n            {trigger(false)}\n          </li>,\n          separatorNode(\"sep-more\")\n        )\n      }\n    }\n    row.push(\n      <li key={crumb.id ?? `${crumb.label}-${index}`} className=\"flex min-w-0 items-center\">\n        {crumbItem(crumb, { index, probe: false, current })}\n      </li>\n    )\n  })\n\n  return (\n    // The landmark exists for the menu as well as for the semantics. `relative` makes it the menu's\n    // containing block; it deliberately does not clip, because the list below does — and a panel\n    // hanging off the bottom of its own clipping containing block is painted nowhere at all (0 of 15\n    // points scanned, against 15 of 15 from here).\n    <nav {...props} ref={navRef} aria-label={label} className=\"relative w-full\">\n      <ol\n        ref={listRef}\n        className={cn(\n          // `w-full` is what makes the measurement stable: the width comes from the parent, so a\n          // trail one pixel too wide is clipped rather than pushing the container wider.\n          // `flex-nowrap` is the difference from official, whose `flex-wrap` grows the header by a\n          // line instead (measured: 45.2px at 320px, 70.8px at 240px). `relative` contains the\n          // probe, whose scrollable overflow this clipping then absorbs.\n          //\n          // The padding is load-bearing, and it is the clipping that makes it so: a `focus-visible`\n          // ring is drawn outside the border box, and with the list flush against its crumbs the\n          // ring on the root is cut off. 4px clears a 2px ring on every side, and `measure`\n          // subtracts the inline padding, so the room costs nothing in the arithmetic. A caller\n          // setting `p-0` gets the clipped ring back.\n          \"relative flex w-full flex-nowrap items-center gap-1.5 overflow-hidden p-1 text-sm text-muted-foreground\",\n          className\n        )}\n      >\n        {row}\n\n        {/* The measuring copy: every crumb at its natural width, then one separator and the\n            ellipsis, in the order `measure` reads them. An `<li>` rather than a `<div>` because an\n            `<ol>` may only contain list items, and an out-of-flow `<li>` is still a valid one. */}\n        <li ref={probeRef} aria-hidden=\"true\" style={PROBE_STYLE}>\n          {items.map((crumb, index) =>\n            crumbItem(crumb, {\n              index,\n              probe: true,\n              current: index === total - 1,\n            })\n          )}\n          {separatorNode()}\n          {trigger(true)}\n        </li>\n      </ol>\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\n          // any of them.\n          data-state=\"open\"\n          aria-labelledby={triggerId}\n          onKeyDown={handleMenuKeyDown}\n          // Placed under the control by arithmetic rather than by measuring it: the ellipsis is\n          // always the second box in the row. Verified to the pixel in both writing directions —\n          // `inset-inline-start` counts from the inline start whichever way the text runs.\n          style={{ insetInlineStart: layout ? `${layout.offset}px` : undefined }}\n          className={cn(\n            \"absolute top-full z-50 mt-1 min-w-[12rem] max-w-[min(20rem,100%)] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md\",\n            menuClassName\n          )}\n        >\n          {folded.map((crumb, index) => {\n            const Tag = crumb.href ? \"a\" : \"span\"\n            return (\n              <Tag\n                key={crumb.id ?? crumb.label}\n                ref={menuRef(index) as React.RefObject<HTMLAnchorElement | null>}\n                role=\"menuitem\"\n                {...(crumb.href\n                  ? {\n                      href: crumb.href,\n                      onClick: (event: React.MouseEvent<HTMLAnchorElement>) =>\n                        chooseFolded(index, event),\n                    }\n                  : {})}\n                // Roving: one tab stop for the menu, on the level that has focus. A level with no\n                // page of its own still takes focus here — it is a rung in the hierarchy somebody is\n                // reading, and skipping it makes the list shorter than it looks.\n                tabIndex={index === menuActive ? 0 : -1}\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                  crumb.href ? \"hover:bg-accent hover:text-accent-foreground\" : \"text-muted-foreground\",\n                  menuItemClassName\n                )}\n              >\n                {crumb.icon ? (\n                  <span aria-hidden=\"true\" className=\"flex size-4 shrink-0 items-center justify-center\">\n                    {crumb.icon}\n                  </span>\n                ) : null}\n                <span className=\"truncate\">{crumb.label}</span>\n              </Tag>\n            )\n          })}\n        </div>\n      ) : null}\n    </nav>\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"
}
