{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "overflow-breadcrumb",
  "type": "registry:component",
  "title": "Overflow Breadcrumb",
  "description": "Breadcrumbs that fold their middle crumbs into a menu as space runs out and unfold as it returns, with a last crumb you can rename in place.",
  "categories": [
    "component",
    "Workspace"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/menu.json"
  ],
  "files": [
    {
      "path": "registry/devl/overflow-breadcrumb.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { ChevronRightIcon, EllipsisIcon, FolderIcon, PencilIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type KeyboardEvent, type ReactNode, useLayoutEffect, useRef, useState } from \"react\";\nimport { Menu, MenuItem, MenuPopup, MenuTrigger } from \"@/components/ui/menu\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface Crumb {\n  id: string;\n  label: string;\n  icon?: ReactNode;\n}\n\nexport interface OverflowBreadcrumbProps {\n  segments: Crumb[];\n  onNavigate?: (crumb: Crumb, index: number) => void;\n  onRename?: (label: string) => void;\n  maxCurrentWidth?: number;\n  className?: string;\n}\n\nconst SEP = 20;\nconst MORE = 28;\n\nexport function OverflowBreadcrumb({ segments, onNavigate, onRename, maxCurrentWidth = 200, className }: OverflowBreadcrumbProps) {\n  const reduce = useReducedMotion();\n  const navRef = useRef<HTMLElement>(null);\n  const measureRef = useRef<HTMLDivElement>(null);\n  const [width, setWidth] = useState(0);\n  const [sizes, setSizes] = useState<number[]>([]);\n  const [editing, setEditing] = useState(false);\n\n  useLayoutEffect(() => {\n    const el = navRef.current;\n    if (!el) return;\n    const ro = new ResizeObserver(() => setWidth(el.clientWidth));\n    ro.observe(el);\n    setWidth(el.clientWidth);\n    return () => ro.disconnect();\n  }, []);\n\n  // Measure every crumb once, offstage, so collapsing never depends on what is currently shown.\n  useLayoutEffect(() => {\n    const row = measureRef.current;\n    if (!row) return;\n    setSizes(Array.from(row.children, (c) => (c as HTMLElement).offsetWidth));\n  }, [segments.map((s) => s.label).join(\"\\u0000\")]);\n\n  const n = segments.length;\n  const lastIndex = n - 1;\n  let shown = segments.map((_, i) => i);\n  if (sizes.length === n && width > 0 && n > 2) {\n    const current = Math.min(sizes[lastIndex], maxCurrentWidth) + (onRename ? 18 : 0);\n    const all = current + sizes.slice(0, lastIndex).reduce((a, b) => a + b + SEP, 0);\n    if (all > width) {\n      let budget = width - current - MORE - SEP;\n      const keep = new Set<number>([lastIndex]);\n      if (sizes[0] + SEP <= budget) {\n        keep.add(0);\n        budget -= sizes[0] + SEP;\n      }\n      for (let i = lastIndex - 1; i >= 1; i--) {\n        if (sizes[i] + SEP > budget) break;\n        keep.add(i);\n        budget -= sizes[i] + SEP;\n      }\n      shown = [...keep].sort((a, b) => a - b);\n    }\n  }\n  const hidden = segments.map((_, i) => i).filter((i) => i !== lastIndex && !shown.includes(i));\n  const moreAt = hidden.length ? (shown[0] === 0 ? 1 : 0) : -1;\n\n  type Slot = { key: string; kind: \"crumb\"; index: number } | { key: string; kind: \"more\" };\n  const slots: Slot[] = shown.map((i) => ({ key: segments[i].id, kind: \"crumb\" as const, index: i }));\n  if (moreAt >= 0) slots.splice(moreAt, 0, { key: \"__more\", kind: \"more\" });\n\n  const ease = [0.23, 1, 0.32, 1] as const;\n  const item = {\n    layout: reduce ? false : (\"position\" as const),\n    initial: reduce ? { opacity: 0 } : { opacity: 0, scale: 0.96 },\n    animate: { opacity: 1, scale: 1 },\n    exit: { opacity: 0, scale: 0.96, transition: { duration: 0.1 } },\n    transition: { duration: 0.18, ease },\n  };\n\n  return (\n    <nav aria-label=\"Breadcrumb\" className={cn(\"relative min-w-0\", className)} ref={navRef}>\n      <div aria-hidden className=\"pointer-events-none invisible absolute inset-x-0 top-0 h-0 overflow-hidden\">\n        <div className=\"flex w-max whitespace-nowrap\" ref={measureRef}>\n          {segments.map((s) => (\n            <span className=\"flex h-7 items-center gap-1.5 px-1.5 font-medium text-sm [&_svg]:size-4\" key={s.id}>\n              {s.icon}\n              {s.label}\n            </span>\n          ))}\n        </div>\n      </div>\n      <ol className=\"flex min-w-0 items-center\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {slots.map((slot, k) => {\n            const sep = k > 0 && (\n              <ChevronRightIcon aria-hidden className=\"mx-0.5 size-4 shrink-0 text-muted-foreground/60\" />\n            );\n            if (slot.kind === \"more\") {\n              return (\n                <motion.li className=\"flex shrink-0 items-center\" key={slot.key} {...item}>\n                  {sep}\n                  <Menu>\n                    <MenuTrigger\n                      aria-label={`Show ${hidden.length} more`}\n                      className=\"relative flex h-7 w-7 cursor-pointer items-center justify-center rounded-md before:absolute before:-inset-y-1 before:inset-x-0 text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring data-popup-open:bg-accent data-popup-open:text-foreground\"\n                    >\n                      <EllipsisIcon className=\"size-4\" />\n                    </MenuTrigger>\n                    <MenuPopup\n                      align=\"start\"\n                      className=\"min-w-48 transition-[scale,opacity] duration-150 ease-out data-ending-style:opacity-0 data-starting-style:scale-[0.96] data-starting-style:opacity-0\"\n                      sideOffset={6}\n                    >\n                      {hidden.map((i, depth) => (\n                        <MenuItem key={segments[i].id} onClick={() => onNavigate?.(segments[i], i)}>\n                          <span className=\"flex items-center gap-2\" style={{ paddingInlineStart: depth * 10 }}>\n                            {segments[i].icon ?? <FolderIcon className=\"size-4 text-muted-foreground\" />}\n                            {segments[i].label}\n                          </span>\n                        </MenuItem>\n                      ))}\n                    </MenuPopup>\n                  </Menu>\n                </motion.li>\n              );\n            }\n            const s = segments[slot.index];\n            const isLast = slot.index === lastIndex;\n            return (\n              <motion.li className={cn(\"flex items-center\", isLast ? \"min-w-0\" : \"shrink-0\")} key={slot.key} {...item}>\n                {sep}\n                {isLast ? (\n                  <CurrentCrumb\n                    crumb={s}\n                    editing={editing}\n                    maxWidth={maxCurrentWidth}\n                    onEdit={onRename ? setEditing : undefined}\n                    onRename={(label) => {\n                      setEditing(false);\n                      if (label && label !== s.label) onRename?.(label);\n                    }}\n                  />\n                ) : (\n                  <button\n                    className=\"relative flex h-7 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md px-1.5 before:absolute before:-inset-y-1 before:inset-x-0 font-medium text-muted-foreground text-sm outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4\"\n                    onClick={() => onNavigate?.(s, slot.index)}\n                    type=\"button\"\n                  >\n                    {s.icon}\n                    {s.label}\n                  </button>\n                )}\n              </motion.li>\n            );\n          })}\n        </AnimatePresence>\n      </ol>\n    </nav>\n  );\n}\n\nfunction CurrentCrumb({\n  crumb,\n  editing,\n  maxWidth,\n  onEdit,\n  onRename,\n}: {\n  crumb: Crumb;\n  editing: boolean;\n  maxWidth: number;\n  onEdit?: (on: boolean) => void;\n  onRename: (label: string) => void;\n}) {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const [draft, setDraft] = useState(crumb.label);\n  const cancelled = useRef(false);\n\n  useLayoutEffect(() => {\n    if (editing) {\n      setDraft(crumb.label);\n      cancelled.current = false;\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    }\n  }, [editing, crumb.label]);\n\n  const finish = (commit: boolean) => {\n    cancelled.current = true;\n    onRename(commit ? draft.trim() : crumb.label);\n    requestAnimationFrame(() => buttonRef.current?.focus());\n  };\n\n  const onKey = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === \"Enter\") {\n      event.preventDefault();\n      finish(true);\n    } else if (event.key === \"Escape\") {\n      event.preventDefault();\n      finish(false);\n    }\n  };\n\n  if (editing) {\n    return (\n      <input\n        aria-label=\"Page name\"\n        className=\"h-7 min-w-16 rounded-md border bg-background px-1.5 font-medium text-foreground text-sm outline-none ring-ring/30 [field-sizing:content] focus:border-ring focus:ring-3\"\n        maxLength={60}\n        onBlur={() => !cancelled.current && finish(true)}\n        onChange={(e) => setDraft(e.target.value)}\n        onKeyDown={onKey}\n        ref={inputRef}\n        style={{ maxWidth }}\n        value={draft}\n      />\n    );\n  }\n\n  const content = (\n    <>\n      {crumb.icon}\n      <span className=\"truncate\">{crumb.label}</span>\n    </>\n  );\n\n  if (!onEdit) {\n    return (\n      <span aria-current=\"page\" className=\"flex h-7 min-w-0 items-center gap-1.5 px-1.5 font-medium text-foreground text-sm [&_svg]:size-4\" style={{ maxWidth }}>\n        {content}\n      </span>\n    );\n  }\n\n  return (\n    <button\n      aria-current=\"page\"\n      aria-label={`${crumb.label}, rename`}\n      className=\"group/cur relative flex h-7 min-w-0 cursor-text before:absolute before:-inset-y-1 before:inset-x-0 items-center gap-1.5 rounded-md px-1.5 font-medium text-foreground text-sm outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4\"\n      onClick={() => onEdit(true)}\n      ref={buttonRef}\n      style={{ maxWidth: maxWidth + 22 }}\n      title=\"Rename\"\n      type=\"button\"\n    >\n      {content}\n      <PencilIcon className=\"size-3! shrink-0 text-muted-foreground opacity-0 transition-opacity duration-150 group-hover/cur:opacity-100 group-focus-visible/cur:opacity-100\" />\n    </button>\n  );\n}\n"
    }
  ]
}