{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "breadcrumbs",
  "title": "Breadcrumbs",
  "description": "A breadcrumb trail with hover-fill pills, animated path changes, and an overflow that collapses into an expandable popover.",
  "dependencies": ["framer-motion"],
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/breadcrumbs/breadcrumbs.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport * as React from \"react\";\n\nexport type BreadcrumbItem = {\n  label: React.ReactNode;\n  href?: string;\n  icon?: React.ReactNode;\n};\n\nexport type BreadcrumbsProps = Omit<\n  React.HTMLAttributes<HTMLElement>,\n  \"onChange\"\n> & {\n  items: BreadcrumbItem[];\n  /** Collapse the middle when there are more than this many items (0 = never). */\n  maxItems?: number;\n  /** Separator node between crumbs. */\n  separator?: React.ReactNode;\n  /** Allow the collapsed middle to expand into a popover. */\n  collapsible?: boolean;\n  /** Called when a crumb is clicked, with its href. */\n  onNavigate?: (href: string) => void;\n};\n\nconst DefaultSeparator = (\n  <svg\n    aria-hidden=\"true\"\n    viewBox=\"0 0 24 24\"\n    className=\"h-3.5 w-3.5 text-muted-foreground/50\"\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\ntype Entry =\n  | { kind: \"crumb\"; item: BreadcrumbItem; index: number; isLast: boolean }\n  | { kind: \"ellipsis\"; hidden: BreadcrumbItem[] };\n\nlet crumbSeed = 0;\n\nconst Breadcrumbs = React.forwardRef<HTMLElement, BreadcrumbsProps>(\n  (\n    {\n      items,\n      maxItems = 0,\n      separator = DefaultSeparator,\n      collapsible = true,\n      onNavigate,\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const reduceMotion = useReducedMotion();\n    const hoverId = React.useMemo(() => `crumb-hover-${crumbSeed++}`, []);\n    const [expanded, setExpanded] = React.useState(false);\n    const [hovered, setHovered] = React.useState<string | null>(null);\n    const popRef = React.useRef<HTMLLIElement>(null);\n\n    React.useEffect(() => {\n      if (!expanded) return;\n      const onDown = (e: MouseEvent) => {\n        if (popRef.current && !popRef.current.contains(e.target as Node)) {\n          setExpanded(false);\n        }\n      };\n      document.addEventListener(\"mousedown\", onDown);\n      return () => document.removeEventListener(\"mousedown\", onDown);\n    }, [expanded]);\n\n    const spring = reduceMotion\n      ? { duration: 0 }\n      : ({ type: \"spring\", stiffness: 520, damping: 32 } as const);\n\n    // Collapse is structural — independent of whether the ellipsis popover is\n    // open. Gating on `expanded` used to expand the whole trail inline when the\n    // dots were clicked, which made the ellipsis vanish instead of opening the\n    // dropdown the Learn article documents.\n    const collapse = maxItems >= 2 && items.length > maxItems && collapsible;\n\n    const entries: Entry[] = React.useMemo(() => {\n      const last = items.length - 1;\n      const toEntry = (item: BreadcrumbItem, index: number): Entry => ({\n        kind: \"crumb\",\n        item,\n        index,\n        isLast: index === last,\n      });\n      if (!collapse) return items.map(toEntry);\n      const tailCount = maxItems - 1;\n      const head = items.slice(0, 1).map(toEntry);\n      const hidden = items.slice(1, items.length - tailCount);\n      const tail = items\n        .slice(items.length - tailCount)\n        .map((item, i) => toEntry(item, items.length - tailCount + i));\n      return [...head, { kind: \"ellipsis\", hidden }, ...tail];\n    }, [items, collapse, maxItems]);\n\n    // CSS-only mobile collapse: when the JS maxItems collapse isn't active and\n    // there are >2 crumbs, hide the middle crumbs below `sm` and show a static\n    // \"…\" so the trail stays on one line. (JS viewport detection can't be used:\n    // in the docs mobile preview the component is portaled into an iframe but\n    // runs in the parent realm, so only CSS reflects the iframe width.)\n    const mobileCollapse = !collapse && items.length > 2;\n\n    const pillClass =\n      \"relative inline-flex items-center gap-1.5 rounded-lg px-2 py-1 font-medium text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring\";\n\n    const HoverPill = ({ active }: { active: boolean }) =>\n      active ? (\n        <motion.span\n          layoutId={hoverId}\n          transition={spring}\n          className=\"absolute inset-0 rounded-lg bg-accent\"\n        />\n      ) : null;\n\n    const renderCrumb = (\n      item: BreadcrumbItem,\n      isLast: boolean,\n      key: string,\n    ) => {\n      const inner = (\n        <>\n          <HoverPill active={hovered === key} />\n          {item.icon && (\n            <span className=\"relative shrink-0 [transition:transform_200ms_ease] group-hover:scale-110\">\n              {item.icon}\n            </span>\n          )}\n          <span className=\"relative truncate\">{item.label}</span>\n        </>\n      );\n      if (isLast || !item.href) {\n        return (\n          <span\n            aria-current={isLast ? \"page\" : undefined}\n            className={`group ${pillClass} ${isLast ? \"bg-muted text-foreground\" : \"text-muted-foreground\"}`}\n          >\n            {inner}\n          </span>\n        );\n      }\n      return (\n        <a\n          href={item.href}\n          onMouseEnter={() => setHovered(key)}\n          onClick={(e) => {\n            if (onNavigate && item.href) {\n              e.preventDefault();\n              onNavigate(item.href);\n            }\n          }}\n          className={`group ${pillClass} text-muted-foreground [transition:color_150ms_ease] hover:text-foreground`}\n        >\n          {inner}\n        </a>\n      );\n    };\n\n    return (\n      <nav\n        ref={ref}\n        aria-label=\"Breadcrumb\"\n        className={className}\n        onMouseLeave={() => setHovered(null)}\n        {...props}\n      >\n        <motion.ol layout className=\"flex flex-wrap items-center gap-0.5\">\n          <AnimatePresence initial={false}>\n            {entries.map((entry, i) => {\n              const key =\n                entry.kind === \"ellipsis\" ? \"ellipsis\" : `crumb-${entry.index}`;\n              const hideOnMobile =\n                mobileCollapse &&\n                entry.kind === \"crumb\" &&\n                entry.index > 0 &&\n                !entry.isLast;\n              return (\n                <React.Fragment key={key}>\n                  {i > 0 && (\n                    <li\n                      aria-hidden=\"true\"\n                      className={`flex items-center px-0.5${hideOnMobile ? \" max-sm:hidden\" : \"\"}`}\n                    >\n                      {separator}\n                    </li>\n                  )}\n                  {entry.kind === \"crumb\" ? (\n                    <motion.li\n                      layout\n                      initial={reduceMotion ? false : { opacity: 0, x: -6 }}\n                      animate={{ opacity: 1, x: 0 }}\n                      exit={\n                        reduceMotion ? { opacity: 0 } : { opacity: 0, x: -6 }\n                      }\n                      transition={spring}\n                      className={`flex min-w-0 items-center${hideOnMobile ? \" max-sm:hidden\" : \"\"}`}\n                    >\n                      {renderCrumb(entry.item, entry.isLast, key)}\n                    </motion.li>\n                  ) : (\n                    <motion.li\n                      layout\n                      ref={popRef}\n                      className=\"relative flex items-center\"\n                    >\n                      <button\n                        type=\"button\"\n                        aria-label={`Show ${entry.hidden.length} hidden crumbs`}\n                        aria-expanded={expanded}\n                        onMouseEnter={() => setHovered(\"ellipsis\")}\n                        onClick={() => setExpanded((open) => !open)}\n                        className={`group ${pillClass} text-muted-foreground hover:text-foreground`}\n                      >\n                        <HoverPill active={hovered === \"ellipsis\"} />\n                        <span className=\"relative flex items-center gap-0.5\">\n                          <span className=\"h-1 w-1 rounded-full bg-current\" />\n                          <span className=\"h-1 w-1 rounded-full bg-current\" />\n                          <span className=\"h-1 w-1 rounded-full bg-current\" />\n                        </span>\n                      </button>\n                      <AnimatePresence>\n                        {expanded && (\n                          <motion.div\n                            initial={\n                              reduceMotion\n                                ? { opacity: 0 }\n                                : { opacity: 0, scale: 0.92, y: -4 }\n                            }\n                            animate={{ opacity: 1, scale: 1, y: 0 }}\n                            exit={\n                              reduceMotion\n                                ? { opacity: 0 }\n                                : { opacity: 0, scale: 0.92, y: -4 }\n                            }\n                            transition={spring}\n                            className=\"absolute top-full left-0 z-popover mt-1.5 min-w-48 origin-top-left rounded-xl border border-border bg-background p-1 shadow-xl\"\n                          >\n                            <ul className=\"flex flex-col\">\n                              {entry.hidden.map((item, hi) => (\n                                // biome-ignore lint/suspicious/noArrayIndexKey: collapsed crumbs are positional\n                                <li key={`hidden-${hi}-${item.href ?? hi}`}>\n                                  <a\n                                    href={item.href}\n                                    onClick={(e) => {\n                                      if (onNavigate && item.href) {\n                                        e.preventDefault();\n                                        onNavigate(item.href);\n                                      }\n                                      setExpanded(false);\n                                    }}\n                                    className=\"flex items-center gap-2 rounded-lg px-3 py-2 text-muted-foreground text-sm [transition:background-color_150ms_ease,color_150ms_ease] hover:bg-accent hover:text-foreground\"\n                                  >\n                                    {item.icon && (\n                                      <span className=\"shrink-0\">\n                                        {item.icon}\n                                      </span>\n                                    )}\n                                    {item.label}\n                                  </a>\n                                </li>\n                              ))}\n                            </ul>\n                          </motion.div>\n                        )}\n                      </AnimatePresence>\n                    </motion.li>\n                  )}\n                  {mobileCollapse && i === 0 && (\n                    <React.Fragment key=\"mobile-ellipsis\">\n                      <li\n                        aria-hidden=\"true\"\n                        className=\"flex items-center px-0.5 sm:hidden\"\n                      >\n                        {separator}\n                      </li>\n                      <li aria-hidden=\"true\" className=\"sm:hidden\">\n                        <span className={`${pillClass} text-muted-foreground`}>\n                          <span className=\"flex items-center gap-0.5\">\n                            <span className=\"h-1 w-1 rounded-full bg-current\" />\n                            <span className=\"h-1 w-1 rounded-full bg-current\" />\n                            <span className=\"h-1 w-1 rounded-full bg-current\" />\n                          </span>\n                        </span>\n                      </li>\n                    </React.Fragment>\n                  )}\n                </React.Fragment>\n              );\n            })}\n          </AnimatePresence>\n        </motion.ol>\n      </nav>\n    );\n  },\n);\nBreadcrumbs.displayName = \"Breadcrumbs\";\n\nexport { Breadcrumbs };\n",
      "type": "registry:ui",
      "target": "components/godui/breadcrumbs.tsx"
    }
  ],
  "type": "registry:ui"
}
