{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"breadcrumbs","type":"registry:ui","title":"Breadcrumbs","description":"Location trail with chevron, slash or dot separators, a home crumb and long middles collapsed into a menu; in button style a hover pill glides between crumbs.","author":"GetUrUI <hello@geturui.dev>","dependencies":["motion","lucide-react"],"registryDependencies":["@geturui/popover"],"files":[{"path":"registry/geturui/ui/breadcrumbs.tsx","type":"registry:ui","target":"components/ui/breadcrumbs.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { LayoutGroup, motion, useReducedMotion } from \"motion/react\";\nimport { ChevronRight, Home, MoreHorizontal } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Popover } from \"@/components/ui/popover\";\n\nexport interface BreadcrumbItem {\n  label: string;\n  href?: string;\n  /** Leading glyph, e.g. a workspace logo or folder icon. */\n  icon?: React.ReactNode;\n  /** Show only the icon; the label stays available to screen readers. */\n  iconOnly?: boolean;\n  onClick?: (e: React.MouseEvent) => void;\n}\n\nexport interface BreadcrumbLinkProps {\n  href: string;\n  className: string;\n  title?: string;\n  onClick?: (e: React.MouseEvent) => void;\n  onPointerEnter?: () => void;\n  onFocus?: () => void;\n  children: React.ReactNode;\n}\n\nexport interface BreadcrumbsProps extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n  /** Trail from the root to the current page. The last item is the current page. */\n  items: BreadcrumbItem[];\n  separator?: \"chevron\" | \"slash\" | \"dot\";\n  /** \"text\" = quiet links; \"button\" = pill crumbs with a hover highlight that glides between them. */\n  variant?: \"text\" | \"button\";\n  /** Prepend a home crumb (icon only). Pass an href, or true for \"/\". */\n  home?: boolean | string;\n  /** Collapse middle crumbs into a \"…\" menu when there are more than this many. */\n  maxItems?: number;\n  /** Crumbs kept before / after the collapsed group. */\n  itemsBeforeCollapse?: number;\n  itemsAfterCollapse?: number;\n  /** Max width of a single label before it truncates (px). */\n  maxLabelWidth?: number;\n  /** Swap the anchor for your router's link (e.g. next/link). */\n  renderLink?: (props: BreadcrumbLinkProps) => React.ReactNode;\n  /** Accessible name of the nav landmark. */\n  label?: string;\n}\n\ntype Entry = { kind: \"item\"; item: BreadcrumbItem; index: number } | { kind: \"collapsed\"; items: BreadcrumbItem[] };\n\nfunction Separator({ type }: { type: NonNullable<BreadcrumbsProps[\"separator\"]> }) {\n  if (type === \"chevron\") return <ChevronRight aria-hidden className=\"size-3.5 shrink-0 text-ink-subtle\" strokeWidth={2} />;\n  if (type === \"slash\") return <span aria-hidden className=\"shrink-0 select-none px-0.5 text-ink-subtle/70\">/</span>;\n  return <span aria-hidden className=\"mx-1 size-1 shrink-0 rounded-full bg-ink-subtle/70\" />;\n}\n\nconst defaultLink = ({ children, ...rest }: BreadcrumbLinkProps) => <a {...rest}>{children}</a>;\n\n/** Location trail in a nav landmark with chevron, slash or dot separators, a collapsible middle and a gliding hover pill. */\nexport function Breadcrumbs({\n  items: rawItems,\n  separator = \"chevron\",\n  variant = \"text\",\n  home,\n  maxItems = 5,\n  itemsBeforeCollapse = 1,\n  itemsAfterCollapse = 2,\n  maxLabelWidth = 180,\n  renderLink = defaultLink,\n  label = \"Breadcrumb\",\n  className,\n  ...props\n}: BreadcrumbsProps) {\n  const reduce = useReducedMotion();\n  const groupId = React.useId();\n  const [hover, setHover] = React.useState<number | null>(null);\n\n  const items = React.useMemo<BreadcrumbItem[]>(() => {\n    if (!home) return rawItems;\n    return [{ label: \"Home\", href: typeof home === \"string\" ? home : \"/\", icon: <Home className=\"size-4\" strokeWidth={1.9} />, iconOnly: true }, ...rawItems];\n  }, [home, rawItems]);\n\n  const entries = React.useMemo<Entry[]>(() => {\n    const all: Entry[] = items.map((item, index) => ({ kind: \"item\", item, index }));\n    if (items.length <= maxItems || itemsBeforeCollapse + itemsAfterCollapse >= items.length) return all;\n    return [\n      ...all.slice(0, itemsBeforeCollapse),\n      { kind: \"collapsed\", items: items.slice(itemsBeforeCollapse, items.length - itemsAfterCollapse) },\n      ...all.slice(items.length - itemsAfterCollapse),\n    ];\n  }, [items, maxItems, itemsBeforeCollapse, itemsAfterCollapse]);\n\n  const last = items.length - 1;\n  const isButton = variant === \"button\";\n\n  const crumbBase = cn(\n    \"relative inline-flex min-w-0 items-center gap-1.5 rounded-full text-sm font-medium outline-none transition-colors duration-150 focus-visible:shadow-[var(--focus-ring)]\",\n    isButton ? \"h-8 px-2.5\" : \"h-7 px-1\",\n  );\n\n  const highlight = (i: number) =>\n    isButton && hover === i ? (\n      <motion.span\n        layoutId=\"crumb-hover\"\n        aria-hidden\n        className=\"absolute inset-0 rounded-full bg-surface-hover\"\n        transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 480, damping: 36 }}\n      />\n    ) : null;\n\n  const content = (item: BreadcrumbItem) => (\n    <>\n      {item.icon && <span className=\"relative inline-flex shrink-0 items-center [&_svg]:size-4\">{item.icon}</span>}\n      <span className={cn(item.iconOnly ? \"sr-only\" : \"relative truncate\")} style={item.iconOnly ? undefined : { maxWidth: maxLabelWidth }}>\n        {item.label}\n      </span>\n    </>\n  );\n\n  return (\n    <nav aria-label={label} className={cn(\"min-w-0\", className)} {...props}>\n      <LayoutGroup id={groupId}>\n        <ol className=\"flex min-w-0 flex-wrap items-center gap-x-1 gap-y-1\" onPointerLeave={() => setHover(null)}>\n          {entries.map((entry, pos) => {\n            const sep = pos > 0 ? <Separator type={separator} /> : null;\n\n            if (entry.kind === \"collapsed\") {\n              const hiddenCount = entry.items.length;\n              return (\n                <li key=\"collapsed\" className=\"flex items-center gap-1\">\n                  {sep}\n                  <Popover\n                    align=\"start\"\n                    label=\"Hidden pages\"\n                    className=\"min-w-[200px] p-1.5\"\n                    trigger={\n                      <button\n                        type=\"button\"\n                        aria-label={`Show ${hiddenCount} hidden ${hiddenCount === 1 ? \"page\" : \"pages\"}`}\n                        onPointerEnter={() => setHover(-1)}\n                        onFocus={() => setHover(-1)}\n                        className={cn(crumbBase, \"text-ink-muted hover:text-ink\", !isButton && \"hover:bg-surface-hover\", isButton ? \"w-8 justify-center px-0\" : \"w-7 justify-center\")}\n                      >\n                        {highlight(-1)}\n                        <MoreHorizontal aria-hidden className=\"relative size-4\" />\n                      </button>\n                    }\n                  >\n                    {({ close }) => (\n                      <ul\n                        className=\"grid gap-0.5\"\n                        onKeyDown={(e) => {\n                          if (e.key !== \"ArrowDown\" && e.key !== \"ArrowUp\") return;\n                          e.preventDefault();\n                          const links = Array.from(e.currentTarget.querySelectorAll<HTMLElement>(\"a,button\"));\n                          const at = links.indexOf(document.activeElement as HTMLElement);\n                          const next = e.key === \"ArrowDown\" ? (at + 1) % links.length : (at - 1 + links.length) % links.length;\n                          links[next]?.focus();\n                        }}\n                      >\n                        {entry.items.map((item) => (\n                          <li key={item.label}>\n                            {renderLink({\n                              href: item.href ?? \"#\",\n                              className:\n                                \"flex h-9 items-center gap-2 rounded-sm px-2.5 text-sm text-ink-muted outline-none transition-colors hover:bg-surface-hover hover:text-ink focus-visible:bg-surface-hover focus-visible:text-ink\",\n                              title: item.label.length > 28 ? item.label : undefined,\n                              onClick: (e) => {\n                                item.onClick?.(e);\n                                close();\n                              },\n                              children: (\n                                <>\n                                  {item.icon && <span className=\"inline-flex shrink-0 [&_svg]:size-4\">{item.icon}</span>}\n                                  <span className=\"truncate\">{item.label}</span>\n                                </>\n                              ),\n                            })}\n                          </li>\n                        ))}\n                      </ul>\n                    )}\n                  </Popover>\n                </li>\n              );\n            }\n\n            const { item, index } = entry;\n            const current = index === last;\n            const title = !item.iconOnly && item.label.length > 24 ? item.label : undefined;\n\n            return (\n              <li key={`${index}-${item.label}`} className=\"flex min-w-0 items-center gap-1\">\n                {sep}\n                {current || !item.href ? (\n                  <span\n                    aria-current={current ? \"page\" : undefined}\n                    title={title}\n                    className={cn(\n                      crumbBase,\n                      current ? \"font-semibold text-ink\" : \"text-ink-muted\",\n                      current && isButton && \"bg-surface-raised shadow-[inset_0_0_0_1px_var(--color-border)]\",\n                    )}\n                  >\n                    {content(item)}\n                  </span>\n                ) : (\n                  renderLink({\n                    href: item.href,\n                    title,\n                    onClick: item.onClick,\n                    onPointerEnter: () => setHover(index),\n                    onFocus: () => setHover(index),\n                    className: cn(crumbBase, \"text-ink-muted hover:text-ink\", !isButton && \"hover:underline hover:underline-offset-4 decoration-border-strong\"),\n                    children: (\n                      <>\n                        {highlight(index)}\n                        {content(item)}\n                      </>\n                    ),\n                  })\n                )}\n              </li>\n            );\n          })}\n        </ol>\n      </LayoutGroup>\n    </nav>\n  );\n}\n"},{"path":"LICENSE-geturui.md","type":"registry:file","target":"LICENSE-geturui.md","content":"MIT License\n\nCopyright (c) 2026 geturui.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"}],"categories":["components","breadcrumbs","navigation","trail","path","hierarchy"],"meta":{"tier":"free","license":"MIT","docs":"https://geturui.com/docs/breadcrumbs"}}