{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "breadcrumb", "title": "Integrated Breadcrumb", "description": "Flat items-driven Breadcrumb built on shadcn's Breadcrumb markup: automatic current-page detection, optional ellipsis collapsing via maxItems, and a customizable separator.", "registryDependencies": [ "breadcrumb" ], "files": [ { "path": "registry/ui/breadcrumb.tsx", "content": "import { type ComponentProps, Fragment, type ReactNode } from \"react\";\nimport {\n BreadcrumbEllipsis,\n BreadcrumbItem as BreadcrumbItemPrimitive,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n Breadcrumb as BreadcrumbRoot,\n BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface BreadcrumbItem {\n current?: boolean;\n href?: string;\n label: ReactNode;\n}\n\nexport interface BreadcrumbProps\n extends Omit, \"children\"> {\n itemClassName?: string;\n items: BreadcrumbItem[];\n linkClassName?: string;\n listClassName?: string;\n maxItems?: number;\n pageClassName?: string;\n separator?: ReactNode;\n separatorClassName?: string;\n}\n\ninterface Entry {\n index: number;\n item: BreadcrumbItem;\n key: string;\n}\n\nconst ELLIPSIS_KEY = \"__ellipsis__\";\n\n// Prefix with the original index so repeated hrefs / labels still yield unique\n// React keys. The key is a derived string (not the bare index), so it satisfies\n// the noArrayIndexKey lint rule while staying stable for a given item position.\nconst toKey = (item: BreadcrumbItem, index: number): string => {\n if (item.href !== undefined) {\n return `${index}-${item.href}`;\n }\n if (typeof item.label === \"string\") {\n return `${index}-${item.label}`;\n }\n return `breadcrumb-${index}`;\n};\n\nexport const Breadcrumb = ({\n items,\n separator,\n maxItems,\n listClassName,\n itemClassName,\n linkClassName,\n pageClassName,\n separatorClassName,\n ...navProps\n}: BreadcrumbProps) => {\n const lastIndex = items.length - 1;\n const anyExplicitCurrent = items.some((item) => item.current === true);\n\n const entries: Entry[] = items.map((item, index) => ({\n index,\n item,\n key: toKey(item, index),\n }));\n\n const collapsed = maxItems !== undefined && items.length > maxItems;\n\n // When collapsed, keep the first item, an ellipsis, and the last `maxItems - 1`\n // items. tailStart is clamped to 1 so the head and tail can never overlap and the\n // current page (the last item) is always shown.\n const tailCount = maxItems === undefined ? 0 : Math.max(1, maxItems - 1);\n const tailStart = Math.max(items.length - tailCount, 1);\n\n const nodes: (Entry | { key: string })[] = collapsed\n ? [\n ...entries.slice(0, 1),\n { key: ELLIPSIS_KEY },\n ...entries.slice(tailStart),\n ]\n : entries;\n\n const renderEntry = ({ item, index }: Entry) => {\n const isCurrent =\n item.current ?? (!anyExplicitCurrent && index === lastIndex);\n\n if (isCurrent) {\n return (\n \n {item.label}\n \n );\n }\n if (item.href !== undefined) {\n return (\n \n {item.label}\n \n );\n }\n return item.label;\n };\n\n return (\n \n \n {nodes.map((node, position) => (\n \n \n {node.key === ELLIPSIS_KEY ? (\n \n ) : (\n renderEntry(node as Entry)\n )}\n \n {position < nodes.length - 1 && (\n \n {separator}\n \n )}\n \n ))}\n \n \n );\n};\n\nexport default Breadcrumb;\n", "type": "registry:component", "target": "components/easy/breadcrumb.tsx" } ], "type": "registry:component" }