{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "breadcrumb",
  "type": "registry:component",
  "title": "Breadcrumb",
  "description": "The trail of links back up a hierarchy to the page you are on, with the middle folded away when the path runs long.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/panelui/breadcrumb/index.tsx",
      "type": "registry:component",
      "target": "@components/panelui/breadcrumb/index.tsx",
      "content": "'use client';\n\n/**\n * Breadcrumb — the trail of where you are in a hierarchy.\n *\n * A row of links back up the tree, ending in the page you are on. The last\n * crumb is not a link: you cannot navigate to where you already are, so it is a\n * plain, current-marked label rather than a dead target.\n *\n * ```tsx\n * <Breadcrumb>\n *   <Breadcrumb.List>\n *     <Breadcrumb.Item>\n *       <Breadcrumb.Link href=\"/\">Home</Breadcrumb.Link>\n *     </Breadcrumb.Item>\n *     <Breadcrumb.Item>\n *       <Breadcrumb.Link href=\"/projects\">Projects</Breadcrumb.Link>\n *     </Breadcrumb.Item>\n *     <Breadcrumb.Item>\n *       <Breadcrumb.Page>PanelUI</Breadcrumb.Page>\n *     </Breadcrumb.Item>\n *   </Breadcrumb.List>\n * </Breadcrumb>\n * ```\n *\n * ## The row is all you write\n *\n * Separators belong to the component, not to the page. `Breadcrumb.List` drops\n * a chevron between every pair of crumbs it holds and never at the ends, so a\n * trail is just its items — there is no separator to forget, mis-order, or\n * leave dangling after the current page. Change the glyph once with `separator`\n * on the root and every gap follows.\n *\n * ## A deep trail folds, it does not wrap\n *\n * A long path on a narrow screen should still read as one line. Give the root a\n * `maxItems` and the first and last crumbs survive while the middle folds into\n * a single ellipsis. A fold that would hide only one crumb is skipped: it is\n * the same number of slots with one of them made less useful.\n *\n * Hand the root an `onEllipsisClick` and the ellipsis becomes a real button —\n * the handle for a menu of the steps it is standing in for. Without one it is a\n * quiet, static marker.\n *\n * ## The trail ends where the page is\n *\n * `Breadcrumb.Page` carries `aria-current=\"page\"` and is painted in the full\n * foreground. Links before it are muted, and gather a subtle underline on hover\n * or keyboard focus so the whole row stays quiet until it is being read.\n *\n * ## Long labels stay on one line\n *\n * A crumb's label truncates instead of pushing the trail onto a second line,\n * and a string label gets the full text as its `title`, so the cut is never\n * silent. The row sizes itself to the space it is given: the separator and the\n * ellipsis keep their width, and the labels share what is left.\n */\nimport {\n  Children,\n  Fragment,\n  createContext,\n  isValidElement,\n  useContext,\n  type ComponentProps,\n  type ElementType as RenderElementType,\n  type HTMLAttributes,\n  type MouseEventHandler,\n  type ReactElement,\n  type ReactNode,\n} from 'react';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport { tv } from 'tailwind-variants';\nimport { BREADCRUMB_ELLIPSIS, collapseBreadcrumbItems } from './collapse';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nexport type BreadcrumbSize = 'sm' | 'default';\n\nconst breadcrumbVariants = tv({\n  slots: {\n    root: 'w-full min-w-0',\n    // One line, always. A breadcrumb that wraps into a paragraph has stopped\n    // being a trail and become a block of text.\n    list: 'flex min-w-0 items-center',\n    item: 'flex min-w-0 items-center',\n    link: [\n      'inline-block min-w-0 max-w-full truncate rounded-sm text-muted-foreground outline-none',\n      // The underline is always laid out, only coloured in when it matters, so\n      // hovering does not move the label by a pixel.\n      'underline decoration-transparent decoration-1 underline-offset-4',\n      'transition-[color,text-decoration-color] duration-150 ease-out',\n      'hover:text-foreground hover:decoration-current',\n      'focus-visible:text-foreground focus-visible:decoration-current focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n      'motion-reduce:transition-none',\n    ],\n    page: 'inline-block min-w-0 max-w-full truncate font-medium text-foreground',\n    separator: [\n      'flex shrink-0 items-center text-muted-foreground/60 select-none',\n      '[&_svg]:pointer-events-none [&_svg]:shrink-0',\n    ],\n    ellipsis: [\n      'inline-flex shrink-0 items-center justify-center text-muted-foreground',\n      'rounded-md outline-none transition-colors duration-150',\n      'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n      'motion-reduce:transition-none',\n      '[&_svg]:pointer-events-none [&_svg]:shrink-0',\n    ],\n  },\n  variants: {\n    size: {\n      sm: {\n        list: 'gap-1 text-xs',\n        link: 'underline-offset-2',\n        separator: '[&_svg]:size-3',\n        ellipsis: 'h-5 min-w-5 [&_svg]:size-3.5',\n      },\n      default: {\n        list: 'gap-1.5 text-sm sm:gap-2',\n        separator: '[&_svg]:size-3.5',\n        ellipsis: 'h-6 min-w-6 [&_svg]:size-4',\n      },\n    },\n    interactive: {\n      true: {\n        ellipsis: 'cursor-pointer hover:bg-muted hover:text-foreground active:bg-muted/70',\n      },\n    },\n  },\n  defaultVariants: { size: 'default', interactive: false },\n});\n\n/**\n * The density, separator and folding rules a trail shares. Configured once on\n * the root, read by every part below it.\n */\nconst BreadcrumbContext = createContext<{\n  size: BreadcrumbSize;\n  separator?: ReactNode;\n  maxItems?: number;\n  itemsBeforeCollapse: number;\n  itemsAfterCollapse: number;\n  onEllipsisClick?: MouseEventHandler<HTMLElement>;\n  ellipsisOpen?: boolean;\n  ellipsisLabel?: string;\n}>({\n  size: 'default',\n  itemsBeforeCollapse: 1,\n  itemsAfterCollapse: 1,\n});\n\nconst useBreadcrumb = () => useContext(BreadcrumbContext);\n\nconst ChevronRightGlyph = () => (\n  <svg\n    aria-hidden=\"true\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <path d=\"m9 18 6-6-6-6\" />\n  </svg>\n);\n\nconst EllipsisGlyph = () => (\n  <svg aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n    <circle cx=\"5\" cy=\"12\" r=\"1.7\" />\n    <circle cx=\"12\" cy=\"12\" r=\"1.7\" />\n    <circle cx=\"19\" cy=\"12\" r=\"1.7\" />\n  </svg>\n);\n\nexport interface BreadcrumbProps extends ComponentProps<'nav'> {\n  /** Extra classes for the landmark. */\n  className?: string;\n  /** Text density for every crumb. `sm` suits a header bar. Default `default`. */\n  size?: BreadcrumbSize;\n  /**\n   * The glyph `Breadcrumb.List` places between crumbs. Defaults to a chevron;\n   * pass `'/'`, a dot, or any node to change every gap at once.\n   */\n  separator?: ReactNode;\n  /**\n   * Collapse the trail once it holds more than this many crumbs. The first\n   * `itemsBeforeCollapse` and last `itemsAfterCollapse` crumbs survive; the\n   * middle folds into a single ellipsis. Nothing folds when hiding the middle\n   * would not shorten the row.\n   */\n  maxItems?: number;\n  /** How many leading crumbs survive a collapse, at least one. Default 1. */\n  itemsBeforeCollapse?: number;\n  /** How many trailing crumbs survive a collapse, at least one. Default 1. */\n  itemsAfterCollapse?: number;\n  /**\n   * Makes the collapsed ellipsis pressable — the handle for a menu listing the\n   * hidden steps. Without it the ellipsis is a static marker.\n   */\n  onEllipsisClick?: MouseEventHandler<HTMLElement>;\n  /**\n   * Whether the menu the pressable ellipsis opens is showing. Sets\n   * `aria-expanded` on that button, so a screen reader knows the menu is open.\n   */\n  ellipsisOpen?: boolean;\n  /**\n   * What the collapsed ellipsis is called when pressable. Default \"Show hidden\n   * items\"; set it to localize the button the list creates for you.\n   */\n  ellipsisLabel?: string;\n}\n\n/**\n * The trail's landmark. Labelled for assistive tech, so the row is announced\n * as \"Breadcrumb\" rather than an anonymous run of links. Pass `aria-label` to\n * name it after what the hierarchy describes.\n */\nfunction BreadcrumbRoot({\n  size = 'default',\n  separator,\n  maxItems,\n  itemsBeforeCollapse = 1,\n  itemsAfterCollapse = 1,\n  onEllipsisClick,\n  ellipsisOpen,\n  ellipsisLabel,\n  className,\n  children,\n  ...props\n}: BreadcrumbProps) {\n  return (\n    <BreadcrumbContext.Provider\n      value={{ size, separator, maxItems, itemsBeforeCollapse, itemsAfterCollapse, onEllipsisClick, ellipsisOpen, ellipsisLabel }}\n    >\n      <nav\n        data-slot=\"breadcrumb\"\n        aria-label=\"Breadcrumb\"\n        className={breadcrumbVariants({ size }).root({ className })}\n        {...props}\n      >\n        {children}\n      </nav>\n    </BreadcrumbContext.Provider>\n  );\n}\nBreadcrumbRoot.displayName = 'Breadcrumb';\n\nexport interface BreadcrumbListProps extends ComponentProps<'ol'> {\n  /** Extra classes for the crumb row. */\n  className?: string;\n}\n\n/**\n * Flattens fragments so a wrapped run of crumbs counts as the items inside it,\n * and drops a separator passed straight to the list: the list places its own.\n */\nfunction flattenCrumbs(nodes: ReactNode): ReactElement[] {\n  return Children.toArray(nodes).flatMap((node) => {\n    if (!isValidElement(node)) return [];\n    if (node.type === Fragment) {\n      return flattenCrumbs((node.props as { children?: ReactNode }).children);\n    }\n    if (node.type === BreadcrumbSeparator) return [];\n    return [node];\n  });\n}\n\n/**\n * The crumb row. It owns the separators — one between every pair of crumbs and\n * none at the ends — and the folding, so a caller only ever lists items.\n */\nfunction BreadcrumbList({ className, children, ...props }: BreadcrumbListProps) {\n  const { size, maxItems, itemsBeforeCollapse, itemsAfterCollapse, onEllipsisClick, ellipsisLabel } = useBreadcrumb();\n  const crumbs = flattenCrumbs(children);\n  const slots = breadcrumbVariants({ size });\n  const sequence = collapseBreadcrumbItems(crumbs.length, {\n    maxItems,\n    itemsBeforeCollapse,\n    itemsAfterCollapse,\n  });\n\n  return (\n    <ol data-slot=\"breadcrumb-list\" className={slots.list({ className })} {...props}>\n      {sequence.map((slot, position) => (\n        <Fragment key={position}>\n          {position > 0 ? <BreadcrumbSeparator /> : null}\n          {slot === BREADCRUMB_ELLIPSIS ? (\n            <li data-slot=\"breadcrumb-item\" className={slots.item()}>\n              <BreadcrumbEllipsis onClick={onEllipsisClick} label={ellipsisLabel} />\n            </li>\n          ) : (\n            crumbs[slot]\n          )}\n        </Fragment>\n      ))}\n    </ol>\n  );\n}\nBreadcrumbList.displayName = 'Breadcrumb.List';\n\nexport interface BreadcrumbItemProps extends ComponentProps<'li'> {\n  /** Extra classes for the crumb's list item. */\n  className?: string;\n}\n\n/** One crumb: wraps a link or the current page, and keeps its label on one line. */\nfunction BreadcrumbItem({ className, ...props }: BreadcrumbItemProps) {\n  return <li data-slot=\"breadcrumb-item\" className={breadcrumbVariants().item({ className })} {...props} />;\n}\nBreadcrumbItem.displayName = 'Breadcrumb.Item';\n\nexport interface BreadcrumbLinkProps extends Omit<ComponentProps<'a'>, 'className'> {\n  /** Extra classes for the anchor. */\n  className?: string;\n}\n\n/**\n * A navigable crumb — an ancestor you can jump back to. Renders an `<a>`, so\n * `href`, `target` and the rest work as they do anywhere; `render` draws it as\n * your router's own link instead. A string label is truncated to the space\n * available and carries its full text as a `title`.\n */\nfunction BreadcrumbLink({\n  className,\n  children,\n  title,\n  render,\n  ...props\n}: BreadcrumbLinkProps & RenderProp<'a'>) {\n  const { size } = useBreadcrumb();\n  return useRender({\n    defaultTagName: 'a',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'breadcrumb-link',\n        className: breadcrumbVariants({ size }).link({ className }),\n        title: title ?? (typeof children === 'string' ? children : undefined),\n        children,\n      },\n      props\n    ),\n  });\n}\nBreadcrumbLink.displayName = 'Breadcrumb.Link';\n\nexport interface BreadcrumbPageProps extends Omit<ComponentProps<'span'>, 'className'> {\n  /** Extra classes for the label. */\n  className?: string;\n}\n\n/**\n * The trailing crumb: where you are now. Not a link — `aria-current=\"page\"`\n * marks it as the destination, and it is painted in the full foreground so the\n * trail resolves to it. A string label truncates and carries its full text as a\n * `title`, as `Breadcrumb.Link` does.\n */\nfunction BreadcrumbPage({ className, children, title, ...props }: BreadcrumbPageProps) {\n  const { size } = useBreadcrumb();\n  return (\n    <span\n      data-slot=\"breadcrumb-page\"\n      title={title ?? (typeof children === 'string' ? children : undefined)}\n      className={breadcrumbVariants({ size }).page({ className })}\n      {...props}\n      aria-current=\"page\"\n    >\n      {children}\n    </span>\n  );\n}\nBreadcrumbPage.displayName = 'Breadcrumb.Page';\n\nexport interface BreadcrumbSeparatorProps extends Omit<ComponentProps<'li'>, 'children'> {\n  /** Extra classes for the gap. */\n  className?: string;\n  /** Override the glyph for this one gap. Falls back to the root's separator. */\n  children?: ReactNode;\n}\n\n/**\n * The glyph between crumbs. `Breadcrumb.List` inserts it for you; it is public\n * only for the rare hand-assembled trail, where it is a sibling of the items.\n * Hidden from screen readers — the order of the crumbs already says everything.\n */\nfunction BreadcrumbSeparator({ className, children, ...props }: BreadcrumbSeparatorProps) {\n  const { size, separator } = useBreadcrumb();\n  const glyph = children ?? separator;\n\n  return (\n    <li\n      data-slot=\"breadcrumb-separator\"\n      className={breadcrumbVariants({ size }).separator({ className })}\n      {...props}\n      aria-hidden=\"true\"\n      role=\"presentation\"\n    >\n      {glyph ?? <ChevronRightGlyph />}\n    </li>\n  );\n}\nBreadcrumbSeparator.displayName = 'Breadcrumb.Separator';\n\nexport interface BreadcrumbEllipsisProps extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'onClick'> {\n  /** Extra classes for the marker or button. */\n  className?: string;\n  /** A glyph of your own in place of the three dots. */\n  children?: ReactNode;\n  /**\n   * Called when the ellipsis is pressed. Without it the ellipsis is a static\n   * marker; with it, it is a real button for a menu of the hidden steps.\n   */\n  onClick?: MouseEventHandler<HTMLElement>;\n  /** Its accessible name. Default `More` when static, `Show hidden items` when pressable. */\n  label?: string;\n}\n\n/**\n * Stands in for the crumbs a folded trail hides. Static by default; give it an\n * `onClick` (through the root's `onEllipsisClick`) and it becomes the trigger\n * for a menu of the hidden steps. Either way it is labelled, so the fold is not\n * silent to assistive tech.\n */\nfunction BreadcrumbEllipsis({ className, children, onClick, label, ...props }: BreadcrumbEllipsisProps) {\n  const { size, ellipsisOpen } = useBreadcrumb();\n  const slots = breadcrumbVariants({ size, interactive: Boolean(onClick) });\n  const glyph = children ?? <EllipsisGlyph />;\n\n  if (!onClick) {\n    return (\n      <span\n        data-slot=\"breadcrumb-ellipsis\"\n        role=\"img\"\n        aria-label={label ?? 'More'}\n        className={slots.ellipsis({ className })}\n        {...props}\n      >\n        {glyph}\n      </span>\n    );\n  }\n\n  return (\n    <button\n      data-slot=\"breadcrumb-ellipsis\"\n      type=\"button\"\n      aria-label={label ?? 'Show hidden items'}\n      aria-haspopup=\"menu\"\n      aria-expanded={ellipsisOpen}\n      className={slots.ellipsis({ className })}\n      onClick={onClick}\n      {...props}\n    >\n      {glyph}\n    </button>\n  );\n}\nBreadcrumbEllipsis.displayName = 'Breadcrumb.Ellipsis';\n\nexport const Breadcrumb = Object.assign(BreadcrumbRoot, {\n  List: BreadcrumbList,\n  Item: BreadcrumbItem,\n  Link: BreadcrumbLink,\n  Page: BreadcrumbPage,\n  Separator: BreadcrumbSeparator,\n  Ellipsis: BreadcrumbEllipsis,\n});\n\nexport {\n  BreadcrumbList,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n  BreadcrumbEllipsis,\n};\n"
    },
    {
      "path": "components/panelui/breadcrumb/collapse.ts",
      "type": "registry:component",
      "target": "@components/panelui/breadcrumb/collapse.ts",
      "content": "/**\n * Which crumbs survive a folded trail.\n *\n * Kept apart from the component because it is the one part of a breadcrumb with\n * rules worth testing: the first and last crumb always stay, and a fold that\n * hides a single crumb is not a fold — it is the same number of slots with one\n * of them made less useful.\n */\n\n/** The slot a folded run of crumbs collapses into. */\nexport const BREADCRUMB_ELLIPSIS = 'ellipsis' as const;\n\n/** A crumb index, or the one slot that stands in for the hidden ones. */\nexport type BreadcrumbSlot = number | typeof BREADCRUMB_ELLIPSIS;\n\nexport interface BreadcrumbCollapseOptions {\n  /** Fold once the trail holds more than this many crumbs. */\n  maxItems?: number;\n  /** How many leading crumbs survive. At least one, so the trail keeps its root. */\n  itemsBeforeCollapse?: number;\n  /** How many trailing crumbs survive. At least one, so the trail keeps its end. */\n  itemsAfterCollapse?: number;\n}\n\nconst whole = (value: number) => Math.max(0, Math.floor(Number.isFinite(value) ? value : 0));\n\n/**\n * The order of slots a list should draw for `count` crumbs.\n *\n * Returns every index when nothing is folded, so a caller can map the result\n * straight onto its children without a separate path for the common case.\n */\nexport function collapseBreadcrumbItems(\n  count: number,\n  { maxItems, itemsBeforeCollapse = 1, itemsAfterCollapse = 1 }: BreadcrumbCollapseOptions = {}\n): BreadcrumbSlot[] {\n  const total = whole(count);\n  const all = Array.from({ length: total }, (_, index) => index);\n\n  if (maxItems === undefined || !Number.isFinite(maxItems) || total <= whole(maxItems)) {\n    return all;\n  }\n\n  // The ends are not negotiable: an unfolded view without its first crumb has\n  // lost the way home, and one without its last has lost the page it describes.\n  const before = Math.max(1, whole(itemsBeforeCollapse));\n  const after = Math.max(1, whole(itemsAfterCollapse));\n  const hidden = total - before - after;\n\n  // One hidden crumb plus an ellipsis is the same length as keeping the crumb,\n  // and the crumb is more useful. Fold only when at least two disappear.\n  if (hidden < 2) return all;\n\n  return [\n    ...Array.from({ length: before }, (_, index) => index),\n    BREADCRUMB_ELLIPSIS,\n    ...Array.from({ length: after }, (_, index) => total - after + index),\n  ];\n}\n"
    }
  ]
}
