# Breadcrumb (Notchset): prompt.md (v1.0.0)

- id: `breadcrumb` · version 1.0.0 · component · free
- category: Navigation
- build: Base UI (this item also ships a Radix build)
- install (this build): `npx shadcn@latest add https://notchset.dev/r/breadcrumb.json`
- npm dependencies: none
- registry dependencies: utils, @notchset/dropdown-menu, @notchset/tooltip, https://notchset.dev/r/notchset-foundation.json
- docs: https://notchset.dev/components/breadcrumb
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

shadcn's Breadcrumb in Notchset: mono 12px levels with slash separators, the current page in ink and not a link. BreadcrumbTrail takes the path and folds the middle levels into a … menu when it doesn't fit, keeping the first level and the last two; long names truncate and show in full in a tooltip.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: none beyond React.
- Files: `components/ui/notchset/breadcrumb.tsx`; shared code: `lib/beautiful-ui/notchset/breadcrumb.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `dropdown-menu`, `tooltip`, `notchset-foundation`.
- Builds: separate Base UI and Radix files. Install the one that matches the project (see Install): a free item's bare URL installs the Base UI build of it and its dependencies.
- Exports to keep: `Breadcrumb`, `BreadcrumbEllipsis`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbList`, `BreadcrumbPage`, `BreadcrumbSeparator`, `BreadcrumbTrail`, and every exported type.
- CSS: the install adds the notchset foundation (tokens, keyframes, motion levels) to your global stylesheet once. Nothing to import by hand.
- Re-running `add` (or `--overwrite`) re-applies those rules: put overrides in your own CSS, never in the installed rules.
- Tokens: retheme with the `--notchset-*` custom properties (`--notchset-check`, `--notchset-control-edge`, `--notchset-draw-from`, `--notchset-ease-bloom`, `--notchset-ease-glide`, `--notchset-ease-key-down`, `--notchset-ease-key-up`, `--notchset-ease-travel`, `--notchset-fade`, `--notchset-focus-color`, `--notchset-focus-inset`, `--notchset-grow-to`, `--notchset-key-down`, `--notchset-key-up`, `--notchset-life-from`, `--notchset-life-ms`, `--notchset-node-blink`, `--notchset-node-bloom`, `--notchset-node-delay`, `--notchset-plate-color`, `--notchset-rise-from`, `--notchset-rule`, `--notchset-scan-to`, `--notchset-scroll`, `--notchset-sheet-from`, `--notchset-signal`, `--notchset-sweep-to`, `--notchset-travel`). Never add Tailwind colour classes inside the component.

```tsx
import { BreadcrumbTrail } from "@/components/ui/notchset/breadcrumb";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `BreadcrumbTrail` | `breadcrumb` | The whole path from data, with folding. |
| `Breadcrumb / BreadcrumbList / BreadcrumbItem` | `breadcrumb-list` | shadcn's parts, to compose by hand. |
| `BreadcrumbLink / BreadcrumbPage` | `breadcrumb-link` | A level, or the current page. |
| `BreadcrumbSeparator / BreadcrumbEllipsis` | `breadcrumb-separator` | The slash, and the … mark. |

Style a part with `[data-slot="<slot>"]` selectors or its `className`; keep the attributes when editing.

## Sound
- Keep every `data-slot` and `data-sound` attribute: the sound layer reads them.
- Installing this item adds no audio. Nothing plays until the app mounts `SoundProvider` once (install: `npx shadcn@latest add https://notchset.dev/r/notchset-sound.json`, import from `@/components/ui/notchset/sound-provider`); `useSound()` gives `muted` and `setMuted` for a mute control. Without a provider the audio engine never loads.

## Match the original
- Read `components/ui/notchset/breadcrumb.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when
- breadcrumb, path, navigation trail, hierarchy, app bar, Notchset
- Deep hierarchies (workspace, project, agent, run)
- An app bar that shows where you are

### Not when
- Flat sites with one or two levels
- Steps in a flow: use Stepper

## Mistakes
- Don't make the current page a link
- Keep labels short; the full name lives in the tooltip

## Usage

```tsx
import { BreadcrumbTrail } from "@/components/ui/notchset/breadcrumb";

export function AppBar() {
  return (
    <BreadcrumbTrail
      items={[
        { label: "Acme Labs", href: "/" },
        { label: "Atlas", href: "/atlas" },
        { label: "Planner", href: "/atlas/planner" },
        { label: "run_8F3A1" },
      ]}
    />
  );
}
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `items (BreadcrumbTrail)` | `{ label, href?, type? }[]` |  | The path, root first; the last is the current page. |
| `onNavigate (BreadcrumbTrail)` | `(item, event) => void` |  | Called when a level is picked; preventDefault() to route on the client. |
| `maxCrumbWidth (BreadcrumbTrail)` | `number` |  | Where long names truncate (200px). |

Full docs: https://notchset.dev/components/breadcrumb

## Customising
- Colours: the component reads your shadcn tokens (`--background`, `--foreground`, `--border` …), refined by the `--notchset-*` tokens. The signal colour is `--notchset-signal` (it falls back to `--destructive`). Set tokens on `:root`, or on any container to retheme one area.
- Dark mode follows the `.dark` class on an ancestor (the shadcn and next-themes convention).
- Update later by re-running the install with `--overwrite` (review the diff if you edited it). Changelog: https://notchset.dev/r/changelog.json

## Keyboard

| Keys | Action |
|---|---|
| Enter / Space | Open the … menu; arrows move through hidden levels |

## Performance

- Measures once per resize from an invisible copy of the path.

## Responsive

- Folds as the bar narrows; never wraps.

## Motion inventory

| Interaction | What moves |
|---|---|
| … menu | Drops in like every Notchset menu |

## Accessibility contract (preserve when editing)
- nav labelled Breadcrumb with an ordered list
- The current page has aria-current=page and is not a link
- The fold button says how many levels it hides; separators are hidden from screen readers

## Install

Base UI project (a base-* style in components.json):

```bash
npx shadcn@latest add https://notchset.dev/r/breadcrumb.json
```

Radix project (a radix-*, new-york or default style):

```bash
npx shadcn@latest add https://notchset.dev/r/radix-nova/breadcrumb.json
```

Or add the `@notchset` registry to components.json and run `npx shadcn@latest add @notchset/breadcrumb`: the CLI picks the build from your style.

## Credits
- Built on shadcn/ui (https://ui.shadcn.com)

## Source (Base UI build)

### components/ui/notchset/breadcrumb.tsx

```tsx
"use client";

/**
 * Breadcrumb (Notchset) v1.0.0 · Notchset
 * Docs: https://notchset.dev/components/breadcrumb
 * MIT licensed: free to use, change and share.
 */

import * as React from "react";
import { cn } from "@/lib/utils";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/notchset/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/notchset/tooltip";
import {
  BreadcrumbSlash,
  breadcrumbEllipsisClass,
  breadcrumbItemClass,
  breadcrumbLinkClass,
  breadcrumbListClass,
  breadcrumbPageClass,
  useFold,
  useTruncated,
  type Crumb,
  type CrumbNavigate,
} from "@/lib/beautiful-ui/notchset/breadcrumb";

/*
 * Notchset Breadcrumb (Base UI build). shadcn's Breadcrumb parts, in Notchset, plus BreadcrumbTrail:
 * give it the path and it folds the middle levels into a … menu when they don't fit (first level and
 * last two stay), truncates long names with the full name in a tooltip, and makes the last level the
 * current page (aria-current, not a link).
 */

function Breadcrumb(props: React.ComponentProps<"nav">) {
  return <nav aria-label="Breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
  return <ol data-slot="breadcrumb-list" className={cn(breadcrumbListClass, className)} {...props} />;
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
  return <li data-slot="breadcrumb-item" className={cn(breadcrumbItemClass, className)} {...props} />;
}
function BreadcrumbLink({ className, ...props }: React.ComponentProps<"a">) {
  return <a data-slot="breadcrumb-link" className={cn(breadcrumbLinkClass, className)} {...props} />;
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
  return <span data-slot="breadcrumb-page" aria-current="page" className={cn(breadcrumbPageClass, className)} {...props} />;
}
const BreadcrumbSeparator = BreadcrumbSlash;
function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span data-slot="breadcrumb-ellipsis" aria-hidden className={cn("flex h-7 w-8 items-center justify-center font-mono text-[12px]", className)} {...props}>
      …
    </span>
  );
}

/** One crumb: a link (or the current page) that shows its full name in a tooltip only when it's cut. */
function TrailCrumb({ item, current, onNavigate }: { item: Crumb; current: boolean; onNavigate?: CrumbNavigate }) {
  const [ref, cut] = useTruncated<HTMLElement>();
  const node = current ? (
    <span ref={ref as React.Ref<HTMLSpanElement>} aria-current="page" data-slot="breadcrumb-page" tabIndex={cut ? 0 : undefined} className={breadcrumbPageClass}>
      {item.label}
    </span>
  ) : (
    <a ref={ref as React.Ref<HTMLAnchorElement>} href={item.href} data-slot="breadcrumb-link" onClick={(e) => onNavigate?.(item, e)} className={breadcrumbLinkClass}>
      {item.label}
    </a>
  );
  if (!cut) return node;
  return (
    <Tooltip>
      <TooltipTrigger render={node} />
      <TooltipContent side="bottom">{item.label}</TooltipContent>
    </Tooltip>
  );
}

function BreadcrumbTrail({
  items,
  onNavigate,
  maxCrumbWidth = 200,
  className,
  ...props
}: Omit<React.ComponentProps<"nav">, "children" | "onNavigate"> & {
  items: readonly Crumb[];
  /** Called on a level's click; prevent the default to route on the client. */
  onNavigate?: CrumbNavigate;
  /** Longer names truncate at this width (px). */
  maxCrumbWidth?: number;
}) {
  const nav = React.useRef<HTMLElement>(null);
  const mirror = React.useRef<HTMLOListElement>(null);
  const fold = useFold(items.length, nav, mirror);
  const folded = items.slice(1, 1 + fold);
  const shown = fold ? [items[0]!, null, ...items.slice(1 + fold)] : items;
  const style = { "--crumb-max": `${maxCrumbWidth}px` } as React.CSSProperties;
  return (
    <nav ref={nav} aria-label="Breadcrumb" data-slot="breadcrumb" className={cn("relative min-w-0", className)} style={style} {...props}>
      <ol className={breadcrumbListClass} style={style}>
        {shown.map((item, i) => (
          <React.Fragment key={item ? `${item.label}-${i}` : "fold"}>
            {i > 0 && <BreadcrumbSlash />}
            <li className={breadcrumbItemClass}>
              {item ? (
                <TrailCrumb item={item} current={i === shown.length - 1} onNavigate={onNavigate} />
              ) : (
                <DropdownMenu>
                  <DropdownMenuTrigger aria-label={`Show ${folded.length} hidden level${folded.length === 1 ? "" : "s"}`} className={breadcrumbEllipsisClass}>
                    …
                  </DropdownMenuTrigger>
                  <DropdownMenuContent align="start" className="w-[280px]">
                    {folded.map((f, k) => (
                      <DropdownMenuItem key={`${f.label}-${k}`} render={<a href={f.href} />} onClick={(e) => onNavigate?.(f, e)}>
                        <span className="flex w-full min-w-0 items-center gap-2.5" style={{ paddingInlineStart: k * 10 }}>
                          <span aria-hidden className="h-px w-2 flex-none bg-[var(--notchset-control-edge,var(--input))]" />
                          <span className="min-w-0 flex-1 truncate font-mono text-[12px]">{f.label}</span>
                          {f.type && <span className="font-mono text-[12px] tracking-[0.04em] text-muted-foreground">{f.type}</span>}
                        </span>
                      </DropdownMenuItem>
                    ))}
                  </DropdownMenuContent>
                </DropdownMenu>
              )}
            </li>
          </React.Fragment>
        ))}
      </ol>
      {/* The full trail, invisible, measured to decide how much to fold. */}
      <ol ref={mirror} aria-hidden className={cn(breadcrumbListClass, "pointer-events-none invisible absolute start-0 top-0 w-max")} style={style}>
        {items.map((item, i) => (
          <React.Fragment key={`${item.label}-${i}`}>
            {i > 0 && <BreadcrumbSlash />}
            <li data-crumb="" className={breadcrumbItemClass}>
              <span className={i === items.length - 1 ? breadcrumbPageClass : breadcrumbLinkClass}>{item.label}</span>
            </li>
          </React.Fragment>
        ))}
      </ol>
    </nav>
  );
}

export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbTrail };
export type { Crumb, CrumbNavigate };
```

### lib/beautiful-ui/notchset/breadcrumb.tsx

```tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

/*
 * Notchset Breadcrumb, shared by both builds. Mono 12px crumbs: links muted (ink on hover), the current
 * page in ink and medium weight, not a link. Slash separators. Long names truncate at --crumb-max and
 * show in full in a tooltip. BreadcrumbTrail folds the middle levels into a 32×28 … menu when the path
 * doesn't fit, keeping the first level and the last two.
 */

export type Crumb = { label: string; href?: string; /** e.g. PROJECT, AGENT: shown in the fold menu. */ type?: string };

/** A level was picked; call e.preventDefault() to route on the client instead of following its href. */
export type CrumbNavigate = (item: Crumb, e: { preventDefault(): void; defaultPrevented: boolean }) => void;

export const breadcrumbListClass = "m-0 flex min-w-0 list-none flex-nowrap items-center p-0 whitespace-nowrap [--crumb-max:200px]";
export const breadcrumbItemClass = "relative flex min-w-0 items-center";
export const breadcrumbLinkClass = cn(
  `notchset-focus`,
  "block max-w-(--crumb-max) truncate font-mono text-[12px] leading-7 text-muted-foreground no-underline transition-colors duration-(--notchset-fade) hover:text-foreground [--notchset-focus-inset:-4px]",
);
export const breadcrumbPageClass = "block max-w-(--crumb-max) truncate font-mono text-[12px] leading-7 font-medium text-foreground";
export const breadcrumbEllipsisClass = cn(
  `notchset-focus`,
  "flex h-7 w-8 flex-none cursor-pointer items-center justify-center border border-solid border-[var(--notchset-rule,var(--border))] bg-transparent p-0 font-mono text-[12px] font-medium text-foreground transition-colors duration-[140ms] hover:border-foreground data-[popup-open]:border-foreground data-[popup-open]:bg-primary data-[popup-open]:text-primary-foreground data-[state=open]:border-foreground data-[state=open]:bg-primary data-[state=open]:text-primary-foreground [--notchset-focus-inset:-3px]",
);

export function BreadcrumbSlash({ className, ...props }: React.ComponentProps<"li">) {
  return (
    <li role="presentation" aria-hidden data-slot="breadcrumb-separator" className={cn("flex flex-none items-center px-2 text-[var(--notchset-control-edge,var(--input))]", className)} {...props}>
      {props.children ?? (
        <svg width="8" height="14" viewBox="0 0 8 14">
          <path d="M6.5 1.5 L1.5 12.5" fill="none" stroke="currentColor" strokeWidth="1.1" />
        </svg>
      )}
    </li>
  );
}

/**
 * How many middle levels to fold so the trail fits its nav: measured from an invisible copy of the full
 * trail (`mirror`), re-measured when the nav resizes. The first level and the last two never fold.
 */
export function useFold(count: number, nav: React.RefObject<HTMLElement | null>, mirror: React.RefObject<HTMLElement | null>) {
  const [fold, setFold] = React.useState(0);
  React.useLayoutEffect(() => {
    const el = nav.current;
    const m = mirror.current;
    if (!el || !m) return;
    const measure = () => {
      const room = el.clientWidth;
      const widths = [...m.querySelectorAll<HTMLElement>("[data-crumb]")].map((c) => c.getBoundingClientRect().width);
      const sep = m.querySelector<HTMLElement>("[data-slot=breadcrumb-separator]")?.getBoundingClientRect().width ?? 18;
      const more = 32 + sep;
      const total = (hidden: number) => widths.reduce((a, w, i) => a + (i >= 1 && i < 1 + hidden ? 0 : w + (i ? sep : 0)), 0) + (hidden ? more : 0);
      const maxFold = Math.max(0, count - 3);
      let n = 0;
      while (n < maxFold && total(n) > room) n++;
      setFold(n);
    };
    measure();
    const ro = typeof ResizeObserver === "function" ? new ResizeObserver(measure) : null;
    ro?.observe(el);
    return () => ro?.disconnect();
  }, [count, nav, mirror]);
  return fold;
}

/** True when an element's text is cut by its ellipsis (so it earns a tooltip). */
export function useTruncated<T extends HTMLElement>() {
  const ref = React.useRef<T>(null);
  const [cut, setCut] = React.useState(false);
  React.useLayoutEffect(() => {
    const el = ref.current;
    if (!el) return;
    const check = () => setCut(el.scrollWidth > el.clientWidth + 1);
    check();
    const ro = typeof ResizeObserver === "function" ? new ResizeObserver(check) : null;
    ro?.observe(el);
    return () => ro?.disconnect();
  }, []);
  return [ref, cut] as const;
}
```

The notchset foundation (the tokens listed above, keyframes and motion levels) installs once with the first component; its CSS is public at https://notchset.dev/r/notchset-foundation.json.
