{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"breadcrumb","type":"registry:ui","title":"Breadcrumb","description":"A data-driven trail of links with automatic truncation, a decorative separator, and the current page marked for assistive tech.","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["navigation"],"registryDependencies":["https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/breadcrumb/Breadcrumb.tsx","content":"\"use client\";\n\nimport { forwardRef, Fragment } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useSoundCue } from \"../../sound/use-sound\";\n\nexport interface BreadcrumbItem {\n\t/** The crumb's visible text. */\n\tlabel: string;\n\t/** Link target. Ignored on the last item — it is always the current page and never a link. */\n\thref?: string;\n}\n\nexport interface BreadcrumbProps {\n\t/** The full trail, first to last. The last entry is always the current page. Required. */\n\titems: BreadcrumbItem[];\n\t/** Collapse the trail once it holds more than this many items. `0` (the default) never collapses. */\n\tmaxItems?: number;\n\t/** How many leading items stay visible once collapsed. */\n\titemsBeforeCollapse?: number;\n\t/**\n\t * How many trailing items stay visible once collapsed. Floored at `1`\n\t * even if given `0` — the last item is the current page, and it is\n\t * never hidden.\n\t */\n\titemsAfterCollapse?: number;\n\t/** Separator glyph rendered between crumbs. Decorative — never read by a screen reader. */\n\tseparator?: string;\n\t/** Accessible name for the `<nav>`. */\n\tlabel?: string;\n\t/**\n\t * Custom rendering for one crumb, given the item and its index in\n\t * `items`. Overriding this hands you the whole crumb, including\n\t * deciding whether it is a link and whether it carries\n\t * `aria-current` — see the README.\n\t */\n\titem?: (item: BreadcrumbItem, index: number) => ReactNode;\n\t/** Additional CSS classes. */\n\tclassName?: string;\n\t/**\n\t * Plays the select cue through the sound controller when a crumb\n\t * rendered by the default (non-`item`-prop) markup is activated.\n\t * Off by default; only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\ninterface Crumb {\n\tkey: string;\n\titem: BreadcrumbItem;\n\t/** Position in the original `items` array — what decides \"is this the last one\". */\n\tindex: number;\n}\ntype Row = Crumb | { kind: \"ellipsis\"; key: \"ellipsis\" };\n\nfunction toCrumb(entry: BreadcrumbItem, index: number): Crumb {\n\t// Keyed on the original index plus the label rather than the label\n\t// alone: two crumbs can legitimately read the same text (a\n\t// \"Settings / General / General\" trail, say), and the index makes\n\t// each key unique regardless.\n\treturn { key: `${index}-${entry.label}`, item: entry, index };\n}\n\n/**\n * The root element arrives through the ref channel rather than a `ref`\n * prop, per PORTING.md — the Svelte source declares `ref = $bindable(null)`.\n *\n * Rest props are not spread: the Svelte source reads only these props off\n * `$props()` and has no `...restProps`, so the port carries no wider\n * attribute surface than the component it mirrors.\n */\nexport const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>(function Breadcrumb(\n\t{\n\t\titems,\n\t\tmaxItems = 0,\n\t\titemsBeforeCollapse = 1,\n\t\titemsAfterCollapse = 1,\n\t\tseparator = \"/\",\n\t\tlabel = \"Breadcrumb\",\n\t\titem,\n\t\tclassName,\n\t\tsound = false,\n\t},\n\tref\n) {\n\tconst playCue = useSoundCue(sound);\n\n\t// No consumer `onClick` exists on a crumb to forward to — the default\n\t// anchor has none today — so this just plays and returns, the same shape\n\t// as a fancy button with no onClick prop of its own.\n\tfunction handleCrumbClick() {\n\t\tplayCue(\"select\");\n\t}\n\n\t// Collapsing is a decision about the whole list, not something a single\n\t// crumb can make for itself — this is why Breadcrumb renders every item\n\t// itself instead of being a compound of children the caller assembles.\n\tconst rows = ((): Row[] => {\n\t\tconst total = items.length;\n\t\tconst before = Math.max(0, itemsBeforeCollapse);\n\t\t// Floored at 1, not 0: the trailing slice below is the only thing that\n\t\t// can ever hold the last item, and the last item is the current page —\n\t\t// not a crumb this component is willing to hide. `itemsBeforeCollapse`\n\t\t// has no such floor because losing the *first* crumb to \"…\" is a\n\t\t// legitimate, non-destructive collapse (see the README).\n\t\tconst after = Math.max(1, itemsAfterCollapse);\n\n\t\t// Guarded degenerate case: if the visible slice would already cover\n\t\t// (or exceed) the whole trail, collapsing cannot save anything\n\t\t// without either dropping a crumb entirely or showing one twice —\n\t\t// so it is skipped outright and the full trail renders instead.\n\t\tconst shouldCollapse = maxItems > 0 && total > maxItems && before + after < total;\n\n\t\tif (!shouldCollapse) {\n\t\t\treturn items.map(toCrumb);\n\t\t}\n\n\t\tconst leading = items.slice(0, before).map(toCrumb);\n\t\tconst trailing = items\n\t\t\t.slice(total - after)\n\t\t\t.map((entry, offset) => toCrumb(entry, total - after + offset));\n\n\t\treturn [...leading, { kind: \"ellipsis\", key: \"ellipsis\" }, ...trailing];\n\t})();\n\n\tconst lastIndex = items.length - 1;\n\n\treturn (\n\t\t<nav ref={ref} aria-label={label} className={cn(\"ft-breadcrumb\", className)}>\n\t\t\t<ol className=\"flex flex-wrap items-center gap-1.5 text-[13px]\">\n\t\t\t\t{rows.map((row, i) => (\n\t\t\t\t\t<Fragment key={row.key}>\n\t\t\t\t\t\t{i > 0 && (\n\t\t\t\t\t\t\t// Purely decorative: never focusable, never read by a screen reader.\n\t\t\t\t\t\t\t<li aria-hidden=\"true\" className=\"text-muted-foreground/50 select-none\">\n\t\t\t\t\t\t\t\t{separator}\n\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{\"kind\" in row ? (\n\t\t\t\t\t\t\t/*\n\t\t\t\t\t\t\t * The collapsed middle items are omitted from the accessible tree,\n\t\t\t\t\t\t\t * not tucked behind a disclosure — see the README's \"Truncation\"\n\t\t\t\t\t\t\t * section for the consequence and the alternative if that is not\n\t\t\t\t\t\t\t * acceptable for a given trail.\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\t<li aria-hidden=\"true\" className=\"text-muted-foreground select-none\">\n\t\t\t\t\t\t\t\t…\n\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<li className=\"flex items-center\">\n\t\t\t\t\t\t\t\t{item ? (\n\t\t\t\t\t\t\t\t\titem(row.item, row.index)\n\t\t\t\t\t\t\t\t) : row.index === lastIndex ? (\n\t\t\t\t\t\t\t\t\t<span aria-current=\"page\" className=\"text-foreground font-medium\">\n\t\t\t\t\t\t\t\t\t\t{row.item.label}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t) : row.item.href ? (\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\thref={row.item.href}\n\t\t\t\t\t\t\t\t\t\tclassName=\"text-muted-foreground hover:text-foreground transition-colors\"\n\t\t\t\t\t\t\t\t\t\tonClick={handleCrumbClick}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{row.item.label}\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<span className=\"text-muted-foreground\">{row.item.label}</span>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Fragment>\n\t\t\t\t))}\n\t\t\t</ol>\n\t\t</nav>\n\t);\n});\n","type":"registry:ui","target":"@ui/fancy/components/breadcrumb/Breadcrumb.tsx"},{"path":"react/src/components/breadcrumb/index.ts","content":"\"use client\";\n\nexport { Breadcrumb } from \"./Breadcrumb\";\nexport type { BreadcrumbProps, BreadcrumbItem } from \"./Breadcrumb\";\n","type":"registry:ui","target":"@ui/fancy/components/breadcrumb/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/breadcrumb?framework=react"}}