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

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

shadcn's Select in Notchset: a 40px trigger whose value rolls, a panel under a leader with 48px options on a rail, one dot that travels to your pick before the panel closes, and disabled options that say why.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: `@base-ui/react@^1` (Base UI build); `radix-ui@^1` (Radix build).
- Files: `components/ui/notchset/select.tsx`; shared code: `lib/beautiful-ui/notchset/select.tsx`, `lib/beautiful-ui/notchset/menu.tsx`, `lib/beautiful-ui/notchset/instrument.tsx`, `lib/beautiful-ui/notchset/root.ts`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `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: `Select`, `SelectContent`, `SelectGroup`, `SelectItem`, `SelectLabel`, `SelectNote`, `SelectScrollDownButton`, `SelectScrollUpButton`, `SelectSeparator`, `SelectTrigger`, `SelectValue`, 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-travel`, `--notchset-fade`, `--notchset-focus-color`, `--notchset-focus-inset`, `--notchset-life-from`, `--notchset-life-ms`, `--notchset-node-blink`, `--notchset-node-bloom`, `--notchset-node-delay`, `--notchset-plate-color`, `--notchset-rise-from`, `--notchset-rule`, `--notchset-scroll`, `--notchset-signal`, `--notchset-sweep-to`). Never add Tailwind colour classes inside the component.

```tsx
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/notchset/select";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `SelectTrigger` | `select-trigger` | The trigger. |
| `SelectContent` | `select-content` | The panel. |
| `SelectItem` | `select-item` | An option. |

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://beautiful-ui.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/select.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
- select, dropdown select, picker, listbox, model picker, shadcn select, Notchset
- Picking one of a few known options, with a little detail on each

### Not when
- Many options or search: use Combobox

## Mistakes
- Pass items so the trigger can roll the label

## Usage

```tsx
"use client";

import * as React from "react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/notchset/select";

const models = [
  { value: "atlas-3-flash", label: "Atlas 3 Flash" },
  { value: "atlas-3", label: "Atlas 3" },
];

export function ModelSelect() {
  const [model, setModel] = React.useState<string | null>("atlas-3-flash");
  return (
    <Select value={model} onValueChange={setModel} items={models}>
      <SelectTrigger meta="$0.0007 / RUN">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="atlas-3-flash" meta="1.9S P50" tag="DEFAULT">
          Atlas 3 Flash
        </SelectItem>
        <SelectItem value="atlas-3" meta="3.2S P50">
          Atlas 3
        </SelectItem>
      </SelectContent>
    </Select>
  );
}
```

### Radix build

The Radix build's API differs here (asChild instead of render, no Base UI-only parts):

```tsx
"use client";

import * as React from "react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/notchset/select";

export function ModelSelect() {
  const [model, setModel] = React.useState("atlas-3-flash");
  return (
    <Select value={model} onValueChange={setModel}>
      <SelectTrigger meta="$0.0007 / RUN">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="atlas-3-flash" meta="1.9S P50" tag="DEFAULT">
          Atlas 3 Flash
        </SelectItem>
        <SelectItem value="atlas-3" meta="3.2S P50">
          Atlas 3
        </SelectItem>
      </SelectContent>
    </Select>
  );
}
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `Select items` | `{ value, label }[] or a map` |  | Labels for the rolling value. |
| `SelectTrigger meta` | `ReactNode` |  | A reading at the trigger's end. |
| `SelectItem meta / tag / disabledReason` | `ReactNode / ReactNode / string` |  | The row's detail, its tag, and why it's off. |
| `SelectNote` | `span` |  | The line under it; shows a disabled option's reason. |

Full docs: https://beautiful-ui.dev/components/select

## 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://beautiful-ui.dev/r/changelog.json

## Keyboard

| Keys | Action |
|---|---|
| ↑ / ↓ / Enter / Space | Open |
| ↑ / ↓ | Move |
| Home / End | First / last |
| Enter / Space | Pick |
| Esc / Tab | Close |

## Performance

- The panel mounts only while open; the rail measures once and on change.

## Responsive

- The panel matches the trigger's width; options truncate their names.

## Motion inventory

| Interaction | What moves |
|---|---|
| Open | The leader drops, the panel unrolls (380ms), the rail draws down (440ms after 160ms) |
| Pick | The dot travels to the row (380ms); the panel closes 400ms later; the value rolls (380ms) |

## Accessibility contract (preserve when editing)
- The Base UI or Radix select: a listbox, typeahead, focus returns to the trigger
- Disabled options are skipped by the arrow keys

## Install

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

```bash
npx shadcn@latest add https://beautiful-ui.dev/r/select.json
```

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

```bash
npx shadcn@latest add https://beautiful-ui.dev/r/radix-nova/select.json
```

Or add the `@beautiful-ui` registry to components.json and run `npx shadcn@latest add @beautiful-ui/select`: the CLI picks the build from your style.

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

## Source (Base UI build)

### components/ui/notchset/select.tsx

```tsx
"use client";

/**
 * Select (Notchset) v1.0.0 · Beautiful UI, by Chimpy (https://x.com/stochastichimp)
 * Docs: https://beautiful-ui.dev/components/select · Agent prompt: https://beautiful-ui.dev/md/select.md
 * MIT licensed: free to use, change and share.
 */

import * as React from "react";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { cn } from "@/lib/utils";
import { NOTCHSET_ROOT, OWN_SOUND, playCue, useNotchsetPortal } from "@/lib/beautiful-ui/notchset/instrument";
import { MenuBody, menuLabelClass, menuSeparatorClass } from "@/lib/beautiful-ui/notchset/menu";
import { SelectChevron, SelectItemFace, SelectLeader, SelectNode, SelectRail, SelectRoll, selectContentClass, selectItemClass, selectTriggerClass } from "@/lib/beautiful-ui/notchset/select";

/*
 * Notchset Select (Base UI build). shadcn's Select, same parts, in Notchset: a 40px trigger whose value
 * rolls as it changes, a panel that unrolls under a leader, and 48px options on a rail with one dot
 * that travels to the picked option; the panel closes 400ms later so the move is seen. A disabled
 * option can say why (disabledReason), shown by SelectNote. ↑↓ open and move (skipping disabled),
 * Home/End jump, Enter or Space picks, Esc or Tab closes.
 */

const withClass =
  <S,>(base: string, className: string | ((state: S) => string | undefined) | undefined) =>
  (state: S) =>
    cn(base, typeof className === "function" ? className(state) : className);

type Items = Record<string, React.ReactNode> | readonly { value: string; label: React.ReactNode }[];
const SelectContext = React.createContext<{ label: (v: unknown) => React.ReactNode; note: string; setNote: (n: string) => void } | null>(null);

/**
 * Labels read from the Select's own <SelectItem> children, so a plain shadcn composition (no `items`)
 * shows "Atlas 3 Flash" rather than "atlas-3-flash" before the popup has ever opened.
 */
function itemLabels(node: React.ReactNode, out = new Map<string, React.ReactNode>()) {
  React.Children.forEach(node, (child) => {
    if (!React.isValidElement(child)) return;
    const props = child.props as { value?: unknown; children?: React.ReactNode };
    if (child.type === SelectItem && typeof props.value === "string") out.set(props.value, props.children);
    else if (props.children) itemLabels(props.children, out);
  });
  return out;
}

function Select({ items, open: openProp, onOpenChange, defaultOpen = false, children, ...props }: SelectPrimitive.Root.Props<string> & { items?: Items }) {
  const [own, setOwn] = React.useState(defaultOpen);
  const open = openProp ?? own;
  const [note, setNote] = React.useState("");
  const close = React.useRef<ReturnType<typeof setTimeout>>(undefined);
  React.useEffect(() => () => clearTimeout(close.current), []);
  React.useEffect(() => {
    if (!note) return;
    const t = setTimeout(() => setNote(""), 1800);
    return () => clearTimeout(t);
  }, [note]);
  const inferred = React.useMemo(() => (items ? new Map<string, React.ReactNode>() : itemLabels(children)), [items, children]);
  const label = React.useCallback(
    (v: unknown) => {
      if (v == null) return null;
      if (Array.isArray(items)) return (items as { value: string; label: React.ReactNode }[]).find((i) => i.value === v)?.label ?? String(v);
      if (items) return (items as Record<string, React.ReactNode>)[String(v)] ?? String(v);
      return inferred.get(String(v)) ?? String(v);
    },
    [items, inferred],
  );
  return (
    <SelectContext value={{ label, note, setNote }}>
      <SelectPrimitive.Root
        data-slot="select"
        items={items as SelectPrimitive.Root.Props<string>["items"]}
        open={open}
        onOpenChange={(next, details) => {
          clearTimeout(close.current);
          onOpenChange?.(next, details);
          if (next) {
            playCue(document.activeElement, "tick");
            setOwn(true);
          } else if (details.reason === "item-press") {
            // Let the dot travel to the picked option before the panel rolls up.
            close.current = setTimeout(() => setOwn(false), 400);
          } else setOwn(false);
        }}
        {...props}
      >
        {children}
      </SelectPrimitive.Root>
    </SelectContext>
  );
}

function SelectGroup(props: SelectPrimitive.Group.Props) {
  return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}

function SelectValue({ className, placeholder, ...props }: SelectPrimitive.Value.Props & { placeholder?: React.ReactNode }) {
  const ctx = React.useContext(SelectContext);
  return (
    <SelectPrimitive.Value data-slot="select-value" className={withClass("flex min-w-0 flex-1", className)} {...props}>
      {(value: unknown) => <SelectRoll>{value == null || value === "" ? <span className="text-muted-foreground">{placeholder}</span> : (ctx?.label(value) ?? String(value))}</SelectRoll>}
    </SelectPrimitive.Value>
  );
}

function SelectTrigger({ className, children, meta, size = "default", ...props }: SelectPrimitive.Trigger.Props & { meta?: React.ReactNode; size?: "sm" | "default" }) {
  return (
    <SelectPrimitive.Trigger
      {...OWN_SOUND}
      data-slot="select-trigger"
      data-size={size}
      className={withClass(cn(`notchset-focus`, selectTriggerClass, size === "sm" && "h-8"), className)}
      {...props}
    >
      <SelectNode />
      {children}
      {meta && <span className="flex-none font-mono text-[9px] tracking-[0.04em] whitespace-nowrap text-muted-foreground tabular-nums">{meta}</span>}
      <SelectPrimitive.Icon className="flex">
        <SelectChevron />
      </SelectPrimitive.Icon>
    </SelectPrimitive.Trigger>
  );
}

function SelectContent({ className, children, side = "bottom", sideOffset = 8, align = "start", container, ...props }: SelectPrimitive.Popup.Props & Pick<SelectPrimitive.Positioner.Props, "side" | "sideOffset" | "align"> & { container?: SelectPrimitive.Portal.Props["container"] }) {
  const scoped = useNotchsetPortal();
  return (
    <SelectPrimitive.Portal container={container ?? scoped}>
      <SelectPrimitive.Positioner className="isolate z-50 outline-none" side={side} sideOffset={sideOffset} align={align} alignItemWithTrigger={false} collisionPadding={8}>
        <SelectPrimitive.Popup {...NOTCHSET_ROOT} data-slot="select-content" className={withClass(selectContentClass, className)} {...props}>
          <SelectLeader />
          <div className="min-h-0 overflow-hidden">
            <SelectPrimitive.List className="relative">
              <MenuBody className="py-1">
                {children}
                <SelectRail selectedAttr="data-selected" />
              </MenuBody>
            </SelectPrimitive.List>
          </div>
        </SelectPrimitive.Popup>
      </SelectPrimitive.Positioner>
    </SelectPrimitive.Portal>
  );
}

function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
  return <SelectPrimitive.GroupLabel data-slot="select-label" className={withClass(menuLabelClass, className)} {...props} />;
}

function SelectItem({
  className,
  children,
  meta,
  tag,
  signalTag,
  disabledReason,
  disabled,
  onPointerDown,
  ...props
}: SelectPrimitive.Item.Props & { meta?: React.ReactNode; tag?: React.ReactNode; signalTag?: boolean; disabledReason?: string }) {
  const ctx = React.useContext(SelectContext);
  return (
    <SelectPrimitive.Item
      {...OWN_SOUND}
      data-slot="select-item"
      disabled={disabled}
      title={disabled ? disabledReason : undefined}
      onPointerDown={(e) => {
        onPointerDown?.(e);
        if (disabled && disabledReason) {
          playCue(e.currentTarget, "signal");
          ctx?.setNote(disabledReason);
        }
      }}
      className={withClass(selectItemClass, className)}
      {...props}
    >
      <SelectItemFace meta={meta} tag={tag} signalTag={signalTag}>
        <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
      </SelectItemFace>
    </SelectPrimitive.Item>
  );
}

function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
  return <SelectPrimitive.Separator data-slot="select-separator" className={withClass(menuSeparatorClass, className)} {...props} />;
}

/** The line under the select: your helper, or a disabled option's reason in signal for a moment. */
function SelectNote({ className, children, ...props }: React.ComponentProps<"span">) {
  const ctx = React.useContext(SelectContext);
  return (
    <span
      aria-live="polite"
      data-slot="select-note"
      className={cn("font-mono text-[9px] tracking-[0.04em] whitespace-nowrap", ctx?.note ? "text-[var(--notchset-signal-text,var(--destructive))]" : "text-muted-foreground", className)}
      {...props}
    >
      {ctx?.note || children}
    </span>
  );
}

function SelectScrollUpButton(props: SelectPrimitive.ScrollUpArrow.Props) {
  return <SelectPrimitive.ScrollUpArrow data-slot="select-scroll-up-button" {...props} />;
}
function SelectScrollDownButton(props: SelectPrimitive.ScrollDownArrow.Props) {
  return <SelectPrimitive.ScrollDownArrow data-slot="select-scroll-down-button" {...props} />;
}

export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectNote, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue };
```

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

```tsx
"use client";

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

/*
 * Notchset Select parts, shared by both builds (handoff 1.10). The 40px trigger (a node, the value
 * rolling up as it changes, a muted meta and a chevron that turns), the panel under an 8px leader,
 * 48px options with a ring in a 40px gutter, an edge rail through the rings that draws down as the
 * panel opens, and one dot that travels the rail to the picked option before the panel closes.
 */

export const selectTriggerClass = cn(
  "group/select flex h-10 w-full min-w-0 cursor-pointer items-center gap-2.5 border border-solid border-[var(--notchset-control-edge,var(--input))] bg-background px-3 text-start text-foreground outline-none transition-[border-color] duration-[120ms] ease-linear pointer-coarse:h-11",
  "data-[popup-open]:border-foreground data-[state=open]:border-foreground data-[disabled]:cursor-not-allowed data-[disabled]:opacity-40",
);

export function SelectChevron() {
  return (
    <svg
      aria-hidden
      width="9"
      height="9"
      viewBox="0 0 12 12"
      className="flex-none transition-transform duration-300 ease-[cubic-bezier(.34,1.5,.5,1)] group-data-[popup-open]/select:rotate-180 group-data-[state=open]/select:rotate-180"
    >
      <path d="M2.5 4.5 L6 8 L9.5 4.5" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="square" />
    </svg>
  );
}

export function SelectNode() {
  return (
    <svg aria-hidden width="9" height="9" viewBox="0 0 9 9" className="flex-none">
      <circle cx="4.5" cy="4.5" r="3.5" fill="currentColor" />
    </svg>
  );
}

/** The value's 20px window: the old label rolls up and out as the new one rolls up and in (380ms). */
export function SelectRoll({ children }: { children: React.ReactNode }) {
  const key = typeof children === "string" ? children : String(React.Children.count(children));
  const [shown, setShown] = React.useState<{ key: string; node: React.ReactNode; prev: React.ReactNode | null; n: number }>({ key, node: children, prev: null, n: 0 });
  if (shown.key !== key) setShown((s) => ({ key, node: children, prev: s.node, n: s.n + 1 }));
  return (
    <span className="relative block h-5 min-w-0 flex-1 overflow-hidden">
      {shown.prev !== null && (
        <span key={`p${shown.n}`} aria-hidden className="absolute inset-0 flex items-center truncate font-sans text-[13.5px] font-medium motion-safe:animate-[notchset-roll-out_380ms_cubic-bezier(.34,1.3,.5,1)_both] motion-reduce:hidden">
          {shown.prev}
        </span>
      )}
      <span key={`c${shown.n}`} className={cn("absolute inset-0 flex items-center truncate font-sans text-[13.5px] font-medium", shown.n > 0 && "motion-safe:animate-[notchset-roll-in_380ms_cubic-bezier(.34,1.3,.5,1)_both]")}>
        {shown.node}
      </span>
    </span>
  );
}

/** The panel: matches the trigger's width, unrolls (grid rows) under an 8px leader. */
export const selectContentClass = cn(
  "group/menu relative z-50 grid w-(--anchor-width) min-w-(--radix-select-trigger-width) grid-rows-[1fr] border border-solid border-foreground bg-background text-foreground outline-none",
  "transition-[grid-template-rows] duration-[380ms] ease-[var(--notchset-ease-unfold)] delay-[80ms]",
  "data-[starting-style]:grid-rows-[0fr] data-[ending-style]:grid-rows-[0fr] data-[ending-style]:delay-0 data-[ending-style]:duration-[160ms] data-[ending-style]:ease-[var(--notchset-ease-travel)]",
  "motion-safe:data-[state=open]:animate-[notchset-unfold_380ms_var(--notchset-ease-unfold)_80ms_both] motion-safe:data-[state=closed]:animate-[notchset-fold_160ms_var(--notchset-ease-travel)_both]",
);

export function SelectLeader() {
  return <span aria-hidden className="pointer-events-none absolute start-4 -top-[9px] h-2 w-px origin-top bg-foreground motion-safe:animate-[notchset-leader-y_100ms_var(--notchset-ease-travel)_both]" />;
}

/** 48px rows: ring, name over meta, a tag at the end. Disabled rows are muted with a rule ring. */
export const selectItemClass = cn(
  "group/opt relative flex h-12 cursor-pointer items-center gap-2.5 ps-10 pe-3.5 outline-none select-none",
  "data-[disabled]:cursor-not-allowed motion-safe:animate-[notchset-fade-in_160ms_linear_both,notchset-lift-down_340ms_cubic-bezier(.34,1.5,.5,1)_both] [animation-delay:calc(140ms+var(--row,0)*35ms)]",
);

export function SelectItemFace({ children, meta, tag, signalTag }: { children: React.ReactNode; meta?: React.ReactNode; tag?: React.ReactNode; signalTag?: boolean }) {
  return (
    <>
      <svg aria-hidden width="14" height="14" viewBox="0 0 14 14" className="absolute start-3.5 top-[17px] overflow-visible" data-slot="select-ring">
        <circle
          cx="7"
          cy="7"
          r="5"
          strokeWidth="1"
          className="fill-background stroke-[var(--notchset-control-edge,var(--input))] transition-[stroke] duration-[160ms] ease-linear group-data-[selected]/opt:stroke-foreground group-data-[state=checked]/opt:stroke-foreground group-data-[disabled]/opt:stroke-[var(--notchset-rule,var(--border))]"
        />
      </svg>
      <span className="flex min-w-0 flex-1 flex-col gap-[3px]">
        <span className="truncate font-sans text-[13px] font-medium text-foreground group-data-[disabled]/opt:text-muted-foreground">{children}</span>
        {meta && <span className="truncate font-mono text-[8.5px] tracking-[0.04em] text-muted-foreground tabular-nums">{meta}</span>}
      </span>
      {tag && <span className={cn("flex-none font-mono text-[8px] font-medium tracking-[0.1em] whitespace-nowrap", signalTag ? "text-foreground" : "text-muted-foreground")}>{tag}</span>}
    </>
  );
}

/**
 * The rail through every ring and the dot on the selected one. It measures the list it sits in, so
 * it fits any number of options; the dot glides (380ms) when the selection changes.
 */
export function SelectRail({ selectedAttr }: { selectedAttr: "data-selected" | 'data-state="checked"' }) {
  const ref = React.useRef<HTMLSpanElement>(null);
  const [m, setM] = React.useState<{ top: number; h: number; dot: number | null }>({ top: 0, h: 0, dot: null });
  React.useLayoutEffect(() => {
    const list = ref.current?.parentElement;
    if (!list) return;
    const measure = () => {
      const rows = [...list.querySelectorAll<HTMLElement>("[role=option]")];
      if (!rows.length) return;
      const first = rows[0]!;
      const lastRow = rows[rows.length - 1]!;
      const sel = list.querySelector<HTMLElement>(`[role=option][${selectedAttr}]`);
      setM({ top: first.offsetTop + 24, h: lastRow.offsetTop - first.offsetTop, dot: sel ? sel.offsetTop + 21 : null });
    };
    measure();
    const mo = new MutationObserver(measure);
    mo.observe(list, { subtree: true, attributes: true, childList: true, attributeFilter: ["data-selected", "data-state", "aria-selected"] });
    return () => mo.disconnect();
  }, [selectedAttr]);
  return (
    <>
      <span
        ref={ref}
        aria-hidden
        className="pointer-events-none absolute start-[20.5px] w-px origin-top bg-[var(--notchset-control-edge,var(--input))] motion-safe:animate-[notchset-leader-y_440ms_var(--notchset-ease-travel)_160ms_both]"
        style={{ top: m.top, height: m.h }}
      />
      {m.dot !== null && (
        <span
          aria-hidden
          className="pointer-events-none absolute start-[18px] top-0 size-1.5 rounded-full bg-foreground transition-transform duration-[380ms] ease-[var(--notchset-ease-travel)] motion-safe:animate-[notchset-fade-in_160ms_linear_300ms_both]"
          style={{ transform: `translateY(${m.dot}px)` }}
        />
      )}
    </>
  );
}
```

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

```tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";
import { Dot, FlipText, motionOff, playCue, useLatest } from "@/lib/beautiful-ui/notchset/instrument";

/*
 * Notchset menu parts, shared by both builds of Dropdown Menu (and later Context Menu): the classes,
 * the one highlight block that glides to the active row, the row stagger on open, the leaders that
 * follow the side the popup actually opened on, the identity header, and the hold-to-delete row.
 * The primitives (Base UI Menu, Radix DropdownMenu) do the keyboard, type-ahead, focus and submenus.
 */

/** The popup: an ink frame that unfolds (grid rows) under its leader line, and folds away again. */
export const menuContentClass = cn(
  "group/menu relative z-50 grid min-w-[12rem] grid-rows-[1fr] border border-solid border-foreground bg-background text-foreground outline-none",
  "transition-[grid-template-rows] duration-[420ms] ease-[var(--notchset-ease-unfold)] delay-100",
  "data-[starting-style]:grid-rows-[0fr] data-[ending-style]:grid-rows-[0fr] data-[ending-style]:delay-0 data-[ending-style]:duration-200 data-[ending-style]:ease-[var(--notchset-ease-travel)]",
  "motion-safe:data-[state=open]:animate-[notchset-unfold_420ms_var(--notchset-ease-unfold)_100ms_both] motion-safe:data-[state=closed]:animate-[notchset-fold_200ms_var(--notchset-ease-travel)_both]",
);
/** A submenu unfolds sideways (grid columns), with its own leader and node on its near edge. */
export const menuSubContentClass = cn(
  "group/menu relative z-50 grid w-[200px] grid-cols-[1fr] border border-solid border-foreground bg-background text-foreground outline-none",
  "transition-[grid-template-columns] duration-[340ms] ease-[var(--notchset-ease-unfold)] delay-[90ms]",
  "data-[starting-style]:grid-cols-[0fr] data-[ending-style]:grid-cols-[0fr] data-[ending-style]:delay-0 data-[ending-style]:duration-200",
  "motion-safe:data-[state=open]:animate-[notchset-unfold-x_340ms_var(--notchset-ease-unfold)_90ms_both] motion-safe:data-[state=closed]:animate-[notchset-fold-x_200ms_var(--notchset-ease-travel)_both]",
);

/** The scroller inside a popup: it never outgrows the room the primitive reports. */
export const menuScrollerClass =
  "relative max-h-[min(36rem,var(--available-height,var(--radix-dropdown-menu-content-available-height,36rem)))] overflow-x-hidden overflow-y-auto [scrollbar-width:thin]";

/** Rows: 34px, a 14px slot on the start side (focus dot, check or radio), mono 10px caps. */
export const menuItemClass = cn(
  "group/item relative flex h-[34px] cursor-default items-center gap-2.5 ps-4 pe-[14px] font-mono text-[10px] font-medium tracking-[0.08em] uppercase outline-none select-none",
  "data-[disabled]:cursor-not-allowed data-[disabled]:text-[var(--notchset-faint,var(--muted-foreground))]",
  "data-[variant=destructive]:text-[var(--notchset-signal-text,var(--destructive))]",
  "data-[inset]:ps-10 motion-safe:animate-[notchset-row-in_380ms_var(--notchset-ease-settle)_both] [animation-delay:calc(150ms+var(--row,0)*22ms)]",
  "forced-colors:data-[highlighted]:outline forced-colors:data-[highlighted]:outline-2 forced-colors:data-[highlighted]:-outline-offset-2 forced-colors:data-[highlighted]:outline-[Highlight]",
  "[&_svg]:pointer-events-none [&_svg]:shrink-0",
);

export const menuLabelClass = "flex h-6 items-end ps-4 pe-4 pb-1 font-mono text-[8.5px] font-normal tracking-[0.1em] text-muted-foreground uppercase data-[inset]:ps-10";
export const menuSeparatorClass = "my-1 h-px bg-[var(--notchset-rule,var(--border))]";
export const menuShortcutClass =
  "ms-auto flex h-[18px] min-w-[18px] items-center justify-center border border-solid border-[var(--notchset-rule,var(--border))] px-1 font-mono text-[8.5px] font-normal tracking-normal text-muted-foreground transition-[border-color,color] duration-[80ms] group-data-[highlighted]/item:border-[var(--notchset-control-edge,var(--input))] group-data-[highlighted]/item:text-foreground";

/** The focus dot: a 5px SVG circle that scales in on the highlighted row. */
export function MenuFocusDot() {
  return (
    <span aria-hidden data-slot="dropdown-menu-item-dot" className="flex size-3.5 flex-none items-center justify-center">
      <svg width="5" height="5" viewBox="0 0 5 5" className="!size-[5px] scale-0 transition-transform duration-[220ms] ease-[cubic-bezier(.34,1.3,.5,1)] group-data-[highlighted]/item:scale-100">
        <circle cx="2.5" cy="2.5" r="2.5" fill="currentColor" />
      </svg>
    </span>
  );
}

/** The 11px checkbox square: the check traces in when checked; a dash when indeterminate. */
export function MenuCheck() {
  return (
    <svg aria-hidden width="11" height="11" viewBox="0 0 11 11" className="!size-[11px] flex-none overflow-visible">
      <rect x="1" y="1" width="9" height="9" fill="none" stroke="currentColor" strokeWidth="1" />
      <path
        d="M3 5.6 L4.8 7.4 L8.2 3.6"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.4"
        strokeLinecap="square"
        pathLength={1}
        strokeDasharray={1}
        className="[stroke-dashoffset:1] transition-[stroke-dashoffset] duration-[220ms] ease-[var(--notchset-ease-travel)] group-data-[checked]/item:[stroke-dashoffset:0] group-data-[state=checked]/item:[stroke-dashoffset:0]"
      />
      <path d="M3 5.5 H8" stroke="currentColor" strokeWidth="1.4" className="opacity-0 group-data-[state=indeterminate]/item:opacity-100 group-data-[indeterminate]/item:opacity-100" />
    </svg>
  );
}

/** The 11px radio ring; its dot blooms to r 2 when the row is checked. */
export function MenuRadio() {
  return (
    <svg aria-hidden width="11" height="11" viewBox="0 0 11 11" className="!size-[11px] flex-none overflow-visible">
      <circle cx="5.5" cy="5.5" r="4" fill="none" stroke="currentColor" strokeWidth="1" />
      <circle cx="5.5" cy="5.5" fill="currentColor" className="[r:0px] transition-[r] duration-[320ms] ease-[var(--notchset-ease-bloom)] group-data-[checked]/item:[r:2px] group-data-[state=checked]/item:[r:2px]" />
    </svg>
  );
}

/** A 14px slot that holds the row's mark. */
export function MenuSlot({ children }: { children: React.ReactNode }) {
  return <span className="flex size-3.5 flex-none items-center justify-center">{children}</span>;
}

/** The chevron of a submenu row: it nudges 2px while highlighted, and points the other way in RTL. */
export function MenuChevron() {
  return (
    <svg aria-hidden width="10" height="10" viewBox="0 0 12 12" className="ms-auto !size-2.5 transition-transform duration-[260ms] ease-[cubic-bezier(.34,1.6,.5,1)] group-data-[highlighted]/item:translate-x-[2px] rtl:-scale-x-100 rtl:group-data-[highlighted]/item:-translate-x-[2px]">
      <path d="M4.5 2.5 L8 6 L4.5 9.5" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="square" />
    </svg>
  );
}

/**
 * The leader line and the node where it meets the popup, on whichever side the popup actually
 * opened (the primitive's data-side): a menu below draws up to its trigger, above draws down; a
 * submenu draws from its near edge.
 */
export function MenuLeader({ kind = "menu" }: { kind?: "menu" | "sub" }) {
  if (kind === "menu")
    return (
      <>
        <span aria-hidden className="pointer-events-none absolute start-[19.5px] hidden h-[9px] w-px bg-foreground group-data-[side=bottom]/menu:-top-[10px] group-data-[side=bottom]/menu:block group-data-[side=bottom]/menu:origin-bottom group-data-[side=top]/menu:-bottom-[10px] group-data-[side=top]/menu:block group-data-[side=top]/menu:origin-top motion-safe:animate-[notchset-leader-y_120ms_var(--notchset-ease-travel)_both]" />
        <Dot className="start-[16.5px] hidden text-foreground group-data-[side=bottom]/menu:-top-[3.5px] group-data-[side=bottom]/menu:block group-data-[side=top]/menu:-bottom-[3.5px] group-data-[side=top]/menu:block" />
      </>
    );
  return (
    <>
      <span aria-hidden className="pointer-events-none absolute top-4 hidden h-px w-[10px] bg-foreground group-data-[side=left]/menu:-right-[11px] group-data-[side=left]/menu:block group-data-[side=left]/menu:origin-left group-data-[side=right]/menu:-left-[11px] group-data-[side=right]/menu:block group-data-[side=right]/menu:origin-right motion-safe:animate-[notchset-leader-x_110ms_var(--notchset-ease-travel)_both]" />
      <Dot className="top-[13px] hidden text-foreground group-data-[side=left]/menu:-right-[3.5px] group-data-[side=left]/menu:block group-data-[side=right]/menu:-left-[3.5px] group-data-[side=right]/menu:block" />
    </>
  );
}

/**
 * The scroller and its highlight: one 34px field block that glides (170ms) to the highlighted row,
 * signal tint on a destructive row, with a tick as it moves. It also numbers the rows (--row) for the
 * open stagger. Attached when the list itself mounts, so it works on every open.
 */
export function MenuBody({ className, children }: { className?: string; children: React.ReactNode }) {
  const [list, setList] = React.useState<HTMLDivElement | null>(null);
  const block = React.useRef<HTMLSpanElement>(null);
  React.useLayoutEffect(() => {
    const b = block.current;
    if (!list || !b) return;
    let shown: Element | null = null;
    const number = () => list.querySelectorAll<HTMLElement>("[role^=menuitem]").forEach((el, i) => el.style.setProperty("--row", String(i)));
    const place = () => {
      const row = list.querySelector<HTMLElement>("[data-highlighted]");
      if (!row) {
        b.style.opacity = "0";
        shown = null;
        return;
      }
      if (row === shown) return;
      b.style.transition = shown && !motionOff(list) ? "top 170ms cubic-bezier(.2,0,0,1), height 170ms cubic-bezier(.2,0,0,1), background-color 120ms linear, opacity 80ms linear" : "opacity 80ms linear";
      b.style.top = `${row.offsetTop}px`;
      b.style.height = `${row.offsetHeight}px`;
      b.dataset.variant = row.dataset.variant ?? "";
      b.style.opacity = "1";
      if (shown) playCue(row, "tick");
      shown = row;
    };
    number();
    place();
    const mo = new MutationObserver((records) => {
      if (records.some((r) => r.type === "childList")) number();
      place();
    });
    mo.observe(list, { subtree: true, childList: true, attributes: true, attributeFilter: ["data-highlighted"] });
    return () => mo.disconnect();
  }, [list]);
  return (
    <div ref={setList} className={cn(menuScrollerClass, className)}>
      <span
        ref={block}
        aria-hidden
        data-slot="dropdown-menu-highlight"
        className="pointer-events-none absolute inset-x-0 top-0 h-[34px] bg-[var(--notchset-faceplate,var(--accent))] opacity-0 data-[variant=destructive]:bg-[var(--notchset-tint,color-mix(in_oklab,var(--destructive)_14%,var(--background)))]"
      />
      {children}
    </div>
  );
}

/** The identity header: a 13px node, a title in Archivo and a mono meta line, ruled underneath. */
export function MenuHeader({ title, meta, className, ...props }: Omit<React.ComponentProps<"div">, "title"> & { title: React.ReactNode; meta?: React.ReactNode }) {
  return (
    <div data-slot="dropdown-menu-header" className={cn("flex h-[58px] items-center gap-3 border-b border-solid border-[var(--notchset-rule,var(--border))] px-4", className)} {...props}>
      <svg aria-hidden width="13" height="13" viewBox="0 0 13 13" className="flex-none overflow-visible">
        <circle cx="6.5" cy="6.5" r="4" fill="var(--background)" stroke="currentColor" strokeWidth="1" />
        <circle cx="6.5" cy="6.5" r="2" fill="currentColor" />
      </svg>
      <div className="flex min-w-0 flex-col gap-1">
        <span className="font-sans text-sm leading-[1.1] font-medium">{title}</span>
        {meta && <span className="font-mono text-[9px] tracking-[0.02em] whitespace-nowrap text-muted-foreground uppercase">{meta}</span>}
      </div>
    </div>
  );
}

export type HoldLabels = { holding: string; done: string };

export type HoldHandlers = {
  onPointerDown?: React.PointerEventHandler<HTMLElement>;
  onPointerUp?: React.PointerEventHandler<HTMLElement>;
  onPointerCancel?: React.PointerEventHandler<HTMLElement>;
  onLostPointerCapture?: React.PointerEventHandler<HTMLElement>;
  onKeyDown?: React.KeyboardEventHandler<HTMLElement>;
  onKeyUp?: React.KeyboardEventHandler<HTMLElement>;
  onBlur?: React.FocusEventHandler<HTMLElement>;
};

/**
 * The hold-to-delete row's logic: press and hold (pointer, Enter or Space) for `holdMs` and a 2px
 * signal bar fills the row; release, blur, losing the pointer, disabling or unmounting cancels.
 * Each hold is one operation and confirms with the latest callback. Your handlers run first.
 */
export function useMenuHold({ holdMs, onConfirm, disabled, theirs }: { holdMs: number; onConfirm?: () => void; disabled?: boolean; theirs: HoldHandlers }) {
  const [progress, setProgress] = React.useState(0);
  const [phase, setPhase] = React.useState<"idle" | "hold" | "done">("idle");
  const raf = React.useRef(0);
  const op = React.useRef(0);
  const latest = useLatest(onConfirm);
  const cancel = React.useCallback(() => {
    op.current++;
    cancelAnimationFrame(raf.current);
    setPhase((p) => (p === "hold" ? "idle" : p));
    setProgress((p) => (p < 1 ? 0 : p));
  }, []);
  React.useEffect(() => cancel, [disabled, holdMs, cancel]);
  const start = (el: HTMLElement) => {
    if (phase !== "idle" || disabled) return;
    const id = ++op.current;
    playCue(el, "signal");
    const t0 = performance.now();
    setPhase("hold");
    const loop = (now: number) => {
      if (op.current !== id) return;
      const p = Math.min(1, (now - t0) / holdMs);
      setProgress(p);
      if (p < 1) {
        raf.current = requestAnimationFrame(loop);
        return;
      }
      playCue(el, "confirm");
      setPhase("done");
      latest.current?.();
    };
    raf.current = requestAnimationFrame(loop);
  };
  const handlers: Required<HoldHandlers> = {
    onPointerDown: (e) => {
      theirs.onPointerDown?.(e);
      if (e.defaultPrevented || e.button !== 0) return;
      e.currentTarget.setPointerCapture?.(e.pointerId);
      start(e.currentTarget);
    },
    onPointerUp: (e) => {
      theirs.onPointerUp?.(e);
      cancel();
    },
    onPointerCancel: (e) => {
      theirs.onPointerCancel?.(e);
      cancel();
    },
    onLostPointerCapture: (e) => {
      theirs.onLostPointerCapture?.(e);
      cancel();
    },
    onKeyDown: (e) => {
      theirs.onKeyDown?.(e);
      if (e.defaultPrevented) return;
      if ((e.key === "Enter" || e.key === " ") && !e.repeat) {
        e.preventDefault();
        start(e.currentTarget);
      }
    },
    onKeyUp: (e) => {
      theirs.onKeyUp?.(e);
      if (e.key === "Enter" || e.key === " ") cancel();
    },
    onBlur: (e) => {
      theirs.onBlur?.(e);
      cancel();
    },
  };
  return { phase, progress, handlers };
}

/** The hold row's inside: the focus dot, the flipping label and the fill bar. */
export function MenuHoldFace({ phase, progress, children, labels, shortcut }: { phase: "idle" | "hold" | "done"; progress: number; children: string; labels: HoldLabels; shortcut?: React.ReactNode }) {
  return (
    <>
      <MenuFocusDot />
      <FlipText className="min-w-0 flex-1 overflow-hidden">{phase === "hold" ? labels.holding : phase === "done" ? labels.done : children}</FlipText>
      {shortcut}
      <span aria-hidden className="absolute start-0 bottom-0 h-[2px] bg-[var(--notchset-signal-text,var(--destructive))]" style={{ width: `${(progress * 100).toFixed(1)}%` }} />
    </>
  );
}
```

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

```tsx
"use client";

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

/*
 * Notchset instrument parts, shared by every member of a pack: the node (a round light that reports
 * state), the legend printed above a control, the plate a key presses onto, the traced check, the
 * five-tick scanner, and the glyph flip that lands a changed label left to right. Nodes and checks
 * are inline SVG (round at every DPR); the CSS lives in the Notchset foundation. Also: the one motion
 * policy (reduced motion or a data-motion="off" ancestor), ref composition, the portal container
 * and the sound attributes every Notchset root carries.
 */

// ---------------------------------------------------------------------------------------------
// Motion, refs, portals, sound
// ---------------------------------------------------------------------------------------------

/** True under prefers-reduced-motion or inside a data-motion="off" scope. Checked when an effect runs. */
/**
 * Whether motion is off for an element, kept live: it follows the OS reduced-motion setting and any
 * data-motion attribute change above it, so a preference switched mid-playback stops decoration at once.
 */
export function useMotionOff(ref: React.RefObject<Element | null>) {
  const [off, setOff] = React.useState(false);
  React.useEffect(() => {
    const read = () => setOff(motionOff(ref.current));
    const r = requestAnimationFrame(read);
    const mq = window.matchMedia?.("(prefers-reduced-motion: reduce)");
    mq?.addEventListener?.("change", read);
    const mo = new MutationObserver(read);
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ["data-motion"], subtree: true });
    return () => {
      cancelAnimationFrame(r);
      mq?.removeEventListener?.("change", read);
      mo.disconnect();
    };
  }, [ref]);
  return off;
}

/**
 * The microphone, or a rejection after `timeoutMs` (a prompt left open, a denied permission). A stream
 * that arrives after the timeout is stopped at once, so a late "Allow" never leaves the mic on.
 */
export function requestMic(timeoutMs = 5000): Promise<MediaStream> {
  if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) return Promise.reject(new Error("no mic"));
  const ask = navigator.mediaDevices.getUserMedia({ audio: true });
  return new Promise((resolve, reject) => {
    let late = false;
    const t = setTimeout(() => {
      late = true;
      reject(new Error("timeout"));
    }, timeoutMs);
    ask.then(
      (stream) => {
        clearTimeout(t);
        if (late) stream.getTracks().forEach((track) => track.stop());
        else resolve(stream);
      },
      (err) => {
        clearTimeout(t);
        reject(err);
      },
    );
  });
}

export function motionOff(el: Element | null | undefined) {
  if (typeof window === "undefined") return true;
  if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return true;
  return Boolean(el?.closest('[data-motion="off"]'));
}

function assignRef<T>(ref: React.Ref<T> | undefined, node: T | null): () => void {
  if (typeof ref === "function") {
    const cleanup = ref(node);
    return typeof cleanup === "function" ? cleanup : () => ref(null);
  }
  if (ref) (ref as React.RefObject<T | null>).current = node;
  return () => {
    if (ref) (ref as React.RefObject<T | null>).current = null;
  };
}

/** Composes two refs, keeping React 19 ref cleanups. */
export function useComposedRefs<T>(a: React.Ref<T> | undefined, b: React.Ref<T> | undefined): React.RefCallback<T> {
  return React.useCallback(
    (node: T | null) => {
      const release = [assignRef(a, node), assignRef(b, node)];
      return () => release.forEach((f) => f());
    },
    [a, b],
  );
}

/** Runs ours after theirs, unless theirs called preventDefault. */
export function composeHandler<E extends { defaultPrevented: boolean }>(theirs: ((e: E) => void) | undefined, ours: (e: E) => void) {
  return (e: E) => {
    theirs?.(e);
    if (!e.defaultPrevented) ours(e);
  };
}

/** Keeps the latest value in a ref, for callbacks read inside timers and animation frames. */
export function useLatest<T>(value: T) {
  const ref = React.useRef(value);
  React.useLayoutEffect(() => {
    ref.current = value;
  });
  return ref;
}

/*
 * Where Notchset overlays (menus, popovers, tooltips) portal to. Unset, document.body, as shadcn's
 * do. Provide an element to keep them inside a scoped theme: a preview, a widget, a shadow root.
 */
export const NotchsetPortalContext = React.createContext<HTMLElement | null>(null);
export function useNotchsetPortal(): HTMLElement | undefined {
  return React.useContext(NotchsetPortalContext) ?? undefined;
}

export { NOTCHSET_ROOT, OWN_SOUND } from "@/lib/beautiful-ui/notchset/root";

// ---------------------------------------------------------------------------------------------
// Parts
// ---------------------------------------------------------------------------------------------

export type NodeState = "off" | "on" | "live" | "signal";

/** The node: a 9×9 ring whose dot blooms on (`on`), blinks (`live`) or turns signal. */
function Node({ state = "off", delay, className, style, ...props }: Omit<React.ComponentProps<"svg">, "children"> & { state?: NodeState; delay?: number }) {
  return (
    <svg
      aria-hidden
      data-slot="indicator"
      data-state={state}
      width="9"
      height="9"
      viewBox="0 0 9 9"
      className={cn(`notchset-node`, className)}
      style={delay ? ({ "--notchset-node-delay": `${delay}ms`, ...style } as React.CSSProperties) : style}
      {...props}
    >
      <circle data-slot="indicator-ring" cx="4.5" cy="4.5" r="3.25" fill="none" strokeWidth="1" />
      <circle data-slot="indicator-dot" cx="4.5" cy="4.5" />
    </svg>
  );
}

/** A small solid round mark (the node where a leader meets a popup, a pointer): SVG, round at every DPR. */
function Dot({ size = 7, className, style }: { size?: number; className?: string; style?: React.CSSProperties }) {
  return (
    <svg aria-hidden width={size} height={size} viewBox={`0 0 ${size} ${size}`} className={cn("pointer-events-none absolute overflow-visible", className)} style={style}>
      <circle cx={size / 2} cy={size / 2} r={size / 2} fill="currentColor" />
    </svg>
  );
}

/** The legend: a mono label above a control, with an optional node (or any trailing reading). */
function Legend({ label, node, children, className, ...props }: React.ComponentProps<"div"> & { label?: React.ReactNode; node?: NodeState }) {
  return (
    <div data-slot="legend" className={cn(`notchset-legend`, className)} {...props}>
      <span data-slot="legend-label">{label}</span>
      {(children != null || node) && (
        <span data-slot="legend-reading" className="flex items-center gap-2">
          {children}
          {node && <Node state={node} />}
        </span>
      )}
    </div>
  );
}

/** A key on its plate: the plate is an offset frame behind the child; `--notchset-plate-color` tints it. */
function Plated({ className, children, ...props }: React.ComponentProps<"span">) {
  return (
    <span data-slot="plated" className={cn("relative inline-flex", className)} {...props}>
      <span aria-hidden data-slot="plate" className={`notchset-plate`} />
      {children}
    </span>
  );
}

/**
 * The success check. Mounted, it traces itself in over --notchset-check (instant under reduced
 * motion); pass `drawn` to drive the trace yourself.
 */
function Check({ drawn, size = 16 }: { drawn?: boolean; size?: number }) {
  return (
    <svg aria-hidden data-slot="check" width={size} height={size} viewBox="0 0 24 24">
      <path
        d="M4 12.5 L9.5 18 L20 6"
        fill="none"
        stroke="currentColor"
        strokeWidth={1.75}
        strokeLinecap="square"
        pathLength={1}
        strokeDasharray={1}
        className={drawn === undefined ? `notchset-check-draw` : undefined}
        style={drawn === undefined ? undefined : { strokeDashoffset: drawn ? 0 : 1, transition: "stroke-dashoffset var(--notchset-check) var(--notchset-ease-travel)" }}
      />
    </svg>
  );
}

/** The scanner: five 1px ticks fading in turn while something works (static, centre lit, when motion is off). */
function Scanner({ height = 12 }: { height?: number }) {
  return (
    <span aria-hidden data-slot="scanner" className={cn(`notchset-scanner`, "inline-flex items-end gap-[2px]")} style={{ height }}>
      {[0.82, 0.66, 1, 0.66, 0.82].map((h, i) => (
        <span key={i} className="w-px bg-current" style={{ height: height * h, animationDelay: `${i * 140 - 700}ms` }} />
      ))}
    </span>
  );
}

// ---------------------------------------------------------------------------------------------
// Glyph flip: each character cycles through glyphs and lands at 110 + i·14ms, left to right
// ---------------------------------------------------------------------------------------------

const GLYPHS = "ABCDEFGHJKLMNPRSTUVWXYZ0123456789#/+=";
const STILL = new Set([" ", "·", "/", ".", ":"]);

export function scramble(text: string, elapsed: number) {
  let out = "";
  for (let i = 0; i < text.length; i++) {
    const ch = text[i]!;
    out += STILL.has(ch) || elapsed >= 110 + i * 14 ? ch : GLYPHS[(Math.floor(elapsed / 36) * 5 + i * 11) % GLYPHS.length];
  }
  return out;
}

/**
 * The text to show for `text`: when it changes, a few frames of glyphs that land left to right.
 * Runs on requestAnimationFrame only while a flip is active; the settled text is never state. Off
 * under reduced motion, inside data-motion="off" (pass the element) or with `off`.
 */
export function useGlyphFlip(text: string, { off = false, el }: { off?: boolean; el?: React.RefObject<Element | null> } = {}) {
  const [frame, setFrame] = React.useState<{ for: string; shown: string } | null>(null);
  const first = React.useRef(true);
  React.useEffect(() => {
    if (first.current) {
      first.current = false;
      return;
    }
    if (off || motionOff(el?.current)) return;
    let raf = 0;
    const t0 = performance.now();
    const end = 110 + text.length * 14 + 40;
    const step = (now: number) => {
      const elapsed = now - t0;
      if (elapsed >= end) return setFrame(null);
      setFrame({ for: text, shown: scramble(text, elapsed) });
      raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [text, off, el]);
  return frame && frame.for === text ? frame.shown : text;
}

/** A label that flips when its text changes; screen readers read the final text only. */
function FlipText({ children, off, className, ...props }: Omit<React.ComponentProps<"span">, "children"> & { children: string; off?: boolean }) {
  const ref = React.useRef<HTMLSpanElement>(null);
  const shown = useGlyphFlip(children, { off, el: ref });
  return (
    <span ref={ref} data-slot="flip-text" className={cn("relative whitespace-nowrap", className)} {...props}>
      <span aria-hidden>{shown}</span>
      <span className="sr-only">{children}</span>
    </span>
  );
}

// ---------------------------------------------------------------------------------------------
// Sound: Notchset components announce cues; the page's beautiful-ui-sound layer (opt-in) plays them.
// ---------------------------------------------------------------------------------------------

/** The Notchset cues, in the shared sound layer's names (played in the mechanical voice). */
export const NOTCHSET_CUES = { tap: "tap", latch: "lock", signal: "destructive", confirm: "halt", done: "success", tick: "tick" } as const;
export type NotchsetCue = keyof typeof NOTCHSET_CUES;

export function playCue(el: Element | null, cue: NotchsetCue) {
  if (!el || typeof CustomEvent === "undefined" || el.closest('[data-sound="off"]')) return;
  el.dispatchEvent(new CustomEvent("beautiful-ui:sound", { bubbles: true, detail: { cue: NOTCHSET_CUES[cue] } }));
}

// ---------------------------------------------------------------------------------------------
// Shortcuts: "⌘↵" → "Meta+Enter Control+Enter" (aria-keyshortcuts)
// ---------------------------------------------------------------------------------------------

const KEY_NAMES: Record<string, string> = { "⌘": "Meta", "⌃": "Control", "⌥": "Alt", "⇧": "Shift", "↵": "Enter", "⏎": "Enter", "⎋": "Escape", "⌫": "Backspace", "⇥": "Tab", "␣": "Space" };

export function kbdToShortcut(kbd: string | undefined): string | undefined {
  if (!kbd) return undefined;
  const parts = [...kbd.replace(/\s+/g, "")].map((c) => KEY_NAMES[c] ?? c.toUpperCase());
  if (!parts.length) return undefined;
  const combo = parts.join("+");
  // ⌘ on a Mac is Ctrl elsewhere: announce both.
  return combo.includes("Meta") ? `${combo} ${combo.replace("Meta", "Control")}` : combo;
}

/**
 * A number that rolls: each digit is a 0–9 strip; the ones digit moves first and each digit to its left
 * 40ms later (620ms, a small overshoot). `delay` offsets the whole number (rows 60ms apart). Non-digits
 * (",", "$", "%") sit still. Screen readers get the plain value.
 */
function RollingNumber({ value, height = 32, delay = 0, className }: { value: string | number; height?: number; delay?: number; className?: string }) {
  const text = String(value);
  const digits = text.split("");
  const lastDigit = digits.reduce((n, d, i) => (/\d/.test(d) ? i : n), -1);
  return (
    <span className={cn("relative inline-flex overflow-hidden tabular-nums", className)} style={{ height, lineHeight: `${height}px` }}>
      <span className="sr-only">{text}</span>
      <span aria-hidden className="flex">
        {digits.map((d, i) =>
          /\d/.test(d) ? (
            <span
              key={digits.length - i}
              className="flex flex-col transition-transform duration-[620ms] ease-[cubic-bezier(.34,1.15,.5,1)]"
              style={{ transform: `translateY(${-Number(d) * height}px)`, transitionDelay: `${delay + (lastDigit - i) * 40}ms` }}
            >
              {Array.from({ length: 10 }, (_, n) => (
                <span key={n} style={{ height }}>
                  {n}
                </span>
              ))}
            </span>
          ) : (
            <span key={`s${digits.length - i}`}>{d}</span>
          ),
        )}
      </span>
    </span>
  );
}

export { Check, Dot, FlipText, Legend, Node, Plated, RollingNumber, Scanner };
```

### lib/beautiful-ui/notchset/root.ts

```tsx
/*
 * Notchset's root attributes, server-safe (no client directive), so server components can spread
 * them too. Every Notchset root (and portalled popup) carries data-notchset (the motion policy's
 * scope), plays its own cues in the mechanical voice, and keeps the page's click layer out.
 */
export const NOTCHSET_ROOT = { "data-notchset": "", "data-sound": "none", "data-sound-voice": "analogue" } as const;

/** On every interactive element: it plays its own cues, so the page's click layer adds no tap. */
export const OWN_SOUND = { "data-sound": "none" } 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://beautiful-ui.dev/r/notchset-foundation.json.
