# Feature Bento (Notchset): prompt.md (v1.0.0)

- id: `feature-bento` · version 1.0.0 · block · free
- category: Marketing
- build: Base UI (one set of files; its dependencies follow the build)
- install (this build): `npx shadcn@latest add https://beautiful-ui.dev/r/feature-bento.json`
- npm dependencies: class-variance-authority@^0.7
- registry dependencies: utils, https://beautiful-ui.dev/r/notchset-foundation.json
- docs: https://beautiful-ui.dev/components/feature-bento
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

A features bento of four tiles, each shown working: two cursors reviewing a trace while a comment types itself, a command palette that types its own queries until you take over, a cost tile whose weekly bars roll the number on hover or focus, and an automation chain that lights step by step with REPLAY; one tick drives it and stops off screen.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: `class-variance-authority@^0.7`.
- Files: `components/ui/notchset/blocks/feature-bento.tsx`; shared code: `lib/beautiful-ui/notchset/instrument.tsx`, `lib/beautiful-ui/notchset/root.ts`, `lib/beautiful-ui/notchset/button-variants.ts`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `notchset-foundation`.
- Builds: one set of files for both, but its dependencies come in Base UI and Radix builds. 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: `FeatureBento`, 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-signal`, `--notchset-sweep-to`, `--notchset-travel`). Never add Tailwind colour classes inside the component.

```tsx
import { FeatureBento } from "@/components/ui/notchset/blocks/feature-bento";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `FeatureBento` | `feature-bento` | Header and the four tiles. |

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/blocks/feature-bento.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
- bento grid, feature grid, features section, why us, product tour, landing page features, animated features, Notchset
- A landing page section that shows features instead of describing them
- Products whose value is in collaboration, speed and automation

### Not when
- A long feature list: use a table or Comparison Table
- Pages that must stay still: it animates while on screen

## Mistakes
- Keep the copy to one sentence per tile; the animation carries the rest
- Use real commands in the palette so people who click in can try them

## Usage

```tsx
import { FeatureBento } from "@/components/ui/notchset/blocks/feature-bento";

export function Features() {
  return (
    <FeatureBento
      title="Built for the way teams work"
      review={{ title: "Review together", body: "See who is on the trace.", trace: "RUN · TRACE", people: ["Priya", "Maya"], initials: ["PN", "MK"], note: "timeout here", lines: [{ text: "plan", time: "0.4S" }, { text: "search", time: "1.1S" }, { text: "fetch", time: "30S", signal: true }] }}
      palette={{ title: "Keyboard first", body: "Do it without the mouse.", commands: [{ glyph: "R", label: "Open run", kind: "RUN", result: "✓ OPENED" }] }}
      cost={{ title: "Numbers that explain themselves", body: "Hover a week.", label: "COST PER 1K RUNS", weeks: [{ label: "JUL 17", value: 1.6 }, { label: "JUL 24", value: 0.8 }] }}
      automation={{ title: "Automations", body: "The right person hears first.", label: "QUALITY GUARD", steps: [{ kind: "WHEN", text: "Quality drops" }, { kind: "THEN", text: "Page on-call" }] }}
    />
  );
}
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `eyebrow / title / description` | `string / ReactNode / ReactNode` |  | The header. |
| `review` | `{ title, body, trace, lines, people, initials, note }` |  | The shared trace; line 3 is the one selected, and note is the comment typed. |
| `palette` | `{ title, body, commands, autoQueries? }` |  | Commands with a glyph, label, kind and result toast; autoQueries are what it types on its own. |
| `cost` | `{ title, body, label, weeks, marker?, baseline? }` |  | Weekly values; marker draws a dashed line after that week. |
| `automation` | `{ title, body, label, steps, footnote? }` |  | WHEN / THEN steps that light in sequence. |

Full docs: https://beautiful-ui.dev/components/feature-bento

## 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 / Esc | Move, run and clear in the palette |
| Tab | Focus a week's bar to read it |
| Enter | REPLAY the automation |

## Performance

- One 250ms interval for the whole block, paused off screen and in hidden tabs.

## Responsive

- Under 760px (container width) the tiles stack in one column at their own height.

## Motion inventory

| Interaction | What moves |
|---|---|
| Review | Cursors glide (900ms), a line highlight sweeps in, the comment types and posts |
| Palette | Typed queries filter by subsequence; the highlight block moves in 160ms |
| Automation | Nodes fill, checks draw and connectors trace down in sequence |
| Tick | One 250ms tick; stops when hidden, off screen or under reduced motion |

## Accessibility contract (preserve when editing)
- Each tile has a heading; the palette is a combobox with a listbox and an active option, and its result is announced
- Cost bars are focusable and labelled with their week and value; the automation is an ordered list
- Decorative cursors are hidden from assistive tech; under reduced motion every tile shows its finished state

## Install

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

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

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

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

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

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

## Source (Base UI build)

### components/ui/notchset/blocks/feature-bento.tsx

```tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/lib/beautiful-ui/notchset/button-variants";
import { NOTCHSET_ROOT, OWN_SOUND, RollingNumber, motionOff, playCue } from "@/lib/beautiful-ui/notchset/instrument";

/*
 * Notchset Feature Bento (a block, one file for both builds). Four features, each shown working: two people
 * reviewing a trace (cursors glide, a line is selected, a comment types itself and posts); a command
 * palette that types its own queries until you click into it, then filters, moves and runs for real; a
 * cost tile whose weekly bars roll the number when you point at or focus them; and an automation chain
 * that lights step by step, with REPLAY. One 250ms tick drives it all, and it stops when the tab is
 * hidden, the block is off screen or motion is reduced (everything then shows its finished state).
 */

export type BentoCommand = { glyph: string; label: string; kind: string; result: string };
export type BentoTile = { title: string; body: string };

export type FeatureBentoProps = Omit<React.ComponentProps<"section">, "children" | "title"> & {
  eyebrow?: string;
  title: React.ReactNode;
  description?: React.ReactNode;
  review: BentoTile & { trace: string; lines: readonly { text: string; time: string; signal?: boolean }[]; people: readonly [string, string]; initials: readonly [string, string]; note: string };
  palette: BentoTile & { commands: readonly BentoCommand[]; autoQueries?: readonly string[] };
  cost: BentoTile & { label: string; weeks: readonly { label: string; value: number }[]; marker?: { index: number; label: string }; baseline?: number };
  automation: BentoTile & { label: string; steps: readonly { kind: string; text: string }[]; footnote?: string };
};

const small = "font-mono text-[10px] tracking-[0.1em] whitespace-nowrap";
// Subsequence match: "mod" finds "Change model…".
const subsequence = (q: string, s: string) => {
  let i = 0;
  for (const ch of s) if (ch === q[i]) i++;
  return i === q.length;
};
// Cursor positions per phase (x in %, y in px within the trace card): Priya selects line 3, Maya reads.
const PRIYA = [
  [30, 58],
  [52, 104],
  [62, 104],
  [24, 150],
] as const;
const MAYA = [
  [78, 150],
  [70, 70],
  [44, 104],
  [70, 104],
] as const;

/** The tick: counts every 250ms while the block is on screen, visible and allowed to move. */
function useTick(root: React.RefObject<HTMLElement | null>) {
  const [t, setT] = React.useState(12);
  const [still, setStill] = React.useState(false);
  React.useEffect(() => {
    const el = root.current;
    if (!el) return;
    let seen = true;
    const io = typeof IntersectionObserver === "function" ? new IntersectionObserver(([e]) => (seen = !!e?.isIntersecting)) : null;
    io?.observe(el);
    const iv = setInterval(() => {
      if (motionOff(el)) {
        setStill(true);
        return;
      }
      setStill(false);
      if (seen && !document.hidden) setT((n) => n + 1);
    }, 250);
    return () => {
      clearInterval(iv);
      io?.disconnect();
    };
  }, [root]);
  return { t, still };
}

function Tile({ tile, className, children, onEnter, onLeave }: { tile: BentoTile; className?: string; children: React.ReactNode; onEnter?: () => void; onLeave?: () => void }) {
  return (
    <div onMouseEnter={onEnter} onMouseLeave={onLeave} className={cn("flex min-w-0 flex-col gap-3.5 overflow-hidden p-5 transition-colors duration-[160ms] ease-linear hover:bg-[var(--notchset-field,var(--muted))]", className)}>
      {children}
      <div className="flex flex-col gap-1">
        <h3 className="m-0 text-[16px] font-semibold">{tile.title}</h3>
        <p className="m-0 text-[13px] leading-[1.45] text-muted-foreground">{tile.body}</p>
      </div>
    </div>
  );
}

function Review({ review, t, still }: { review: FeatureBentoProps["review"]; t: number; still: boolean }) {
  const phase = still ? 2 : Math.floor(t / 7) % 4;
  const selected = phase === 1 || phase === 2;
  const cycle = t % 28;
  const typed = still ? review.note.length : Math.max(0, Math.min(review.note.length, (cycle - 6) * 3));
  const posted = still || cycle >= 18;
  const showNote = still || cycle >= 5;
  const cursor = (p: readonly (readonly [number, number])[], name: string, ink: boolean) => (
    <div aria-hidden className="pointer-events-none absolute z-[3] transition-[left,top] duration-[900ms] ease-(--notchset-ease-glide)" style={{ left: `${p[phase]![0]}%`, top: p[phase]![1] }}>
      <svg width="12" height="15" viewBox="0 0 12 15">
        <path d="M1 1 L1 12.5 L4 9.6 L6.4 14 L8.4 13 L6.1 8.7 L10.5 8.4 Z" strokeWidth="1" className={ink ? "fill-foreground stroke-background" : "fill-background stroke-foreground"} />
      </svg>
      <span className={cn(small, "absolute top-3 left-[11px] flex h-4 items-center border border-solid border-foreground px-[5px] font-medium tracking-[0.08em]", ink ? "bg-foreground text-background" : "bg-background text-foreground")}>{name}</span>
    </div>
  );
  return (
    <div className="relative box-border min-h-[200px] flex-1 border border-solid border-foreground bg-background px-3.5 py-3">
      <div className={cn(small, "mb-3 flex items-center justify-between tracking-[0.12em] text-muted-foreground")}>
        <span>{review.trace}</span>
        <span aria-hidden className="flex">
          <span className="flex size-5 items-center justify-center rounded-full border-[1.5px] border-solid border-background bg-foreground font-mono text-[10px] text-background">{review.initials[0]}</span>
          <span className="-ms-1.5 box-border flex size-5 items-center justify-center rounded-full border-[1.5px] border-solid border-foreground bg-background font-mono text-[10px] text-foreground">{review.initials[1]}</span>
        </span>
      </div>
      {review.lines.map((l, i) => (
        <div key={l.text} className="relative grid h-[30px] grid-cols-[22px_minmax(0,1fr)_64px] items-center gap-2.5 font-mono text-[11px] whitespace-nowrap">
          <span aria-hidden className={cn("absolute inset-y-1 start-7 end-[72px] origin-left bg-[var(--notchset-field,var(--muted))] transition-[scale] duration-[360ms] ease-(--notchset-ease-unfold)", i === 2 && selected ? "scale-x-100" : "scale-x-0")} />
          <span className="relative text-muted-foreground">{String(i + 1).padStart(2, "0")}</span>
          <span className={cn("relative truncate", l.signal && "text-[var(--notchset-signal-text,var(--destructive))]")}>{l.text}</span>
          <span className={cn("relative text-end", l.signal ? "text-[var(--notchset-signal-text,var(--destructive))]" : "text-muted-foreground")}>{l.time}</span>
        </div>
      ))}
      <div className={cn("absolute end-3.5 bottom-3 box-border flex w-[236px] max-w-[calc(100%-28px)] flex-col gap-1 border border-solid bg-background px-2.5 py-2 transition-[opacity,translate] duration-[360ms] ease-(--notchset-ease-settle)", posted ? "border-foreground" : "border-[var(--notchset-control-edge,var(--input))]", showNote ? "translate-y-0 opacity-100" : "translate-y-1.5 opacity-0")}>
        <span className={cn(small, "font-medium")}>
          {review.people[1].toUpperCase()} · {posted ? "POSTED" : "TYPING"}
        </span>
        <span className="min-h-[18px] text-[13px] leading-[1.4]">
          {review.note.slice(0, typed)}
          {!posted && <span aria-hidden className="ms-px inline-block h-[13px] w-px bg-foreground align-[-2px] motion-safe:animate-[notchset-blink_1s_steps(1)_infinite]" />}
        </span>
      </div>
      {!still && cursor(PRIYA, review.people[0].toUpperCase(), true)}
      {!still && cursor(MAYA, review.people[1].toUpperCase(), false)}
    </div>
  );
}

function Palette({ palette, t, still }: { palette: FeatureBentoProps["palette"]; t: number; still: boolean }) {
  const [manual, setManual] = React.useState(false);
  const [query, setQuery] = React.useState("");
  const [sel, setSel] = React.useState(0);
  const [toast, setToast] = React.useState("");
  const toastTimer = React.useRef<ReturnType<typeof setTimeout>>(undefined);
  React.useEffect(() => () => clearTimeout(toastTimer.current), []);
  const listId = React.useId();
  const auto = palette.autoQueries ?? ["run", "mod", "pau"];
  // Until you click in, it types its own queries and shows what each would run.
  const a = auto[Math.floor(t / 16) % auto.length] ?? "";
  const q = manual ? query : still ? "" : a.slice(0, Math.min(a.length, t % 16));
  const list = palette.commands.filter((c) => !q || subsequence(q.toLowerCase(), c.label.toLowerCase()));
  const index = Math.min(manual ? sel : 0, Math.max(0, list.length - 1));
  const autoToast = !manual && !still && t % 16 >= 9 && t % 16 < 15 ? (list[0]?.result ?? "") : "";
  const shown = manual ? toast : autoToast;
  const run = (c: BentoCommand, el: Element | null) => {
    playCue(el, "done");
    setToast(c.result);
    clearTimeout(toastTimer.current);
    toastTimer.current = setTimeout(() => setToast(""), 1600);
  };
  return (
    <div className="flex flex-1 flex-col justify-center">
      <div className="border border-solid border-foreground bg-background">
        <label className="flex h-11 items-center gap-2.5 border-0 border-b border-solid border-[var(--notchset-rule,var(--border))] px-3">
          <svg aria-hidden width="12" height="12" viewBox="0 0 13 13" className="flex-none">
            <circle cx="5.5" cy="5.5" r="4.2" fill="none" className="stroke-muted-foreground" strokeWidth="1.2" />
            <path d="M8.6 8.6 L12 12" className="stroke-muted-foreground" strokeWidth="1.2" />
          </svg>
          <input
            role="combobox"
            aria-expanded
            aria-controls={listId}
            aria-activedescendant={list.length ? `${listId}-${index}` : undefined}
            aria-label="Search commands"
            value={q}
            onFocus={() => {
              if (!manual) {
                setManual(true);
                setQuery("");
                setSel(0);
              }
            }}
            onBlur={() => {
              if (!query) setManual(false);
            }}
            onChange={(e) => {
              setManual(true);
              setQuery(e.target.value);
              setSel(0);
            }}
            onKeyDown={(e) => {
              if (e.key === "ArrowDown" || e.key === "ArrowUp") {
                e.preventDefault();
                if (!list.length) return;
                playCue(e.currentTarget, "tick");
                setSel((index + (e.key === "ArrowDown" ? 1 : -1) + list.length) % list.length);
              } else if (e.key === "Enter") {
                e.preventDefault();
                const c = list[index];
                if (c) run(c, e.currentTarget);
                else playCue(e.currentTarget, "signal");
              } else if (e.key === "Escape") {
                setQuery("");
                setSel(0);
              }
            }}
            placeholder="Type a command"
            spellCheck={false}
            className="min-w-0 flex-1 border-0 bg-transparent font-mono text-[12px] text-foreground outline-none placeholder:text-muted-foreground"
          />
          <span aria-hidden className="flex h-[18px] items-center border border-solid border-[var(--notchset-rule,var(--border))] px-[5px] font-mono text-[10px] text-muted-foreground">
            ⌘K
          </span>
        </label>
        <div id={listId} role="listbox" aria-label="Commands" className="relative h-[220px] py-1">
          <span aria-hidden className={cn("absolute inset-x-0 h-9 border-0 border-s-2 border-solid border-foreground bg-[var(--notchset-field,var(--muted))] transition-[top] duration-[160ms] ease-(--notchset-ease-glide)", list.length ? "opacity-100" : "opacity-0")} style={{ top: 4 + index * 36 }} />
          {list.map((c, i) => (
            <div
              key={c.label}
              id={`${listId}-${i}`}
              role="option"
              aria-selected={i === index}
              onMouseEnter={() => manual && setSel(i)}
              onMouseDown={(e) => {
                e.preventDefault();
                setManual(true);
                setSel(i);
                run(c, e.currentTarget);
              }}
              className="relative grid h-9 cursor-pointer grid-cols-[22px_minmax(0,1fr)_auto] items-center gap-2.5 px-3"
            >
              <span className={cn("box-border flex size-5 items-center justify-center border border-solid font-mono text-[10px] font-medium", i === index ? "border-foreground bg-foreground text-background" : "border-[var(--notchset-rule,var(--border))] text-muted-foreground")}>{c.glyph}</span>
              <span className="truncate text-[13px]">{c.label}</span>
              <span className={cn(small, "text-muted-foreground")}>{c.kind}</span>
            </div>
          ))}
          {!list.length && <span className={cn(small, "absolute start-3 top-3.5 tracking-[0.06em] whitespace-normal text-muted-foreground")}>NO MATCH · TRY “{(auto[0] ?? "").toUpperCase()}”</span>}
        </div>
        <div className={cn(small, "flex h-9 items-center justify-between gap-2 border-0 border-t border-solid border-[var(--notchset-rule,var(--border))] px-3 tracking-[0.08em] text-muted-foreground")}>
          <span className="@max-[360px]:hidden">↑ ↓ MOVE · ↵ OPEN</span>
          <span aria-live="polite" className={cn("truncate text-foreground transition-opacity duration-200 ease-linear", shown ? "opacity-100" : "opacity-0")}>
            {shown}
          </span>
        </div>
      </div>
    </div>
  );
}

function Cost({ cost }: { cost: FeatureBentoProps["cost"] }) {
  const [hover, setHover] = React.useState(-1);
  const last = cost.weeks.length - 1;
  const at = hover >= 0 ? hover : last;
  const week = cost.weeks[at]!;
  const top = Math.max(...cost.weeks.map((w) => w.value)) * 1.05;
  const base = cost.baseline ?? cost.weeks[0]?.value ?? 1;
  const after = cost.marker && at >= cost.marker.index + 1;
  return (
    <div className="flex flex-1 flex-col gap-3" onMouseLeave={() => setHover(-1)}>
      <div className="flex flex-col gap-1">
        <span className={cn(small, "tracking-[0.12em] text-muted-foreground")}>
          {cost.label} · {week.label}
        </span>
        <span className="flex items-baseline gap-2">
          <RollingNumber value={`$${week.value.toFixed(2)}`} height={34} className="font-mono text-[30px] font-light tracking-[-0.04em]" />
          <span className={cn(small, "flex h-5 items-center bg-foreground px-1.5 font-medium tracking-[0.06em] text-background")}>{after ? `−${Math.round((1 - week.value / base) * 100)}% VS ${cost.weeks[0]?.label.split(" ")[0]}` : "BEFORE THE SWITCH"}</span>
        </span>
      </div>
      <div className="relative grid min-h-[90px] flex-1 grid-cols-[repeat(var(--n),minmax(0,1fr))] items-end gap-1.5 border-0 border-b border-solid border-foreground" style={{ "--n": cost.weeks.length } as React.CSSProperties}>
        {cost.weeks.map((w, i) => (
          <div key={w.label} tabIndex={0} aria-label={`${w.label}: $${w.value.toFixed(2)}`} onMouseEnter={() => setHover(i)} onFocus={() => setHover(i)} className={cn(`notchset-focus`, "relative flex h-full items-end [--notchset-focus-inset:-3px]")}>
            <span className={cn("block w-full transition-colors duration-[140ms] ease-linear", i === at ? "bg-foreground" : cost.marker && i > cost.marker.index ? "bg-muted-foreground" : "bg-[var(--notchset-control-edge,var(--input))]")} style={{ height: `${(w.value / top) * 100}%` }} />
            {cost.marker?.index === i && <span aria-hidden className="absolute inset-y-0 -end-1 border-0 border-e border-dashed border-[var(--notchset-signal,var(--destructive))]" />}
          </div>
        ))}
      </div>
      <div aria-hidden className={cn(small, "flex justify-between gap-2 text-muted-foreground")}>
        <span>{cost.weeks[0]?.label}</span>
        {cost.marker && <span className="truncate text-[var(--notchset-signal-text,var(--destructive))]">{cost.marker.label}</span>}
        <span>{cost.weeks[last]?.label}</span>
      </div>
    </div>
  );
}

function Automation({ automation, t, still }: { automation: FeatureBentoProps["automation"]; t: number; still: boolean }) {
  const [start, setStart] = React.useState(0);
  const n = automation.steps.length;
  const period = (n + 5) * 3;
  const lit = still ? n : Math.min(n, Math.floor(((((t - start) % period) + period) % period) / 3));
  return (
    <div className="flex flex-1 flex-col gap-3">
      <div className={cn(small, "flex items-center justify-between tracking-[0.12em] text-muted-foreground")}>
        <span className="truncate">{automation.label}</span>
        <button
          {...OWN_SOUND}
          type="button"
          onClick={(e) => {
            playCue(e.currentTarget, "tick");
            setStart(t);
          }}
          className={cn(`notchset-focus`, buttonVariants({ variant: "outline", size: "xs" }), "flex-none [--notchset-focus-inset:-3px]")}
        >
          REPLAY
        </button>
      </div>
      <ol className="m-0 flex flex-1 list-none flex-col justify-center p-0">
        {automation.steps.map((s, i) => (
          <li key={s.text} className={cn("relative grid grid-cols-[24px_minmax(0,1fr)] items-start gap-3", i < n - 1 && "pb-[22px]")}>
            {i < n - 1 && (
              <>
                <span aria-hidden className="absolute start-[11.5px] top-6 bottom-0 w-px bg-[var(--notchset-rule,var(--border))]" />
                <span aria-hidden className={cn("absolute start-[11px] top-6 bottom-0 w-0.5 origin-top bg-foreground transition-[scale] duration-[420ms] ease-(--notchset-ease-fold)", i < lit - 1 || (i < lit && lit === n) ? "scale-y-100" : "scale-y-0")} />
              </>
            )}
            <svg aria-hidden width="24" height="24" viewBox="0 0 24 24" className="relative">
              <circle cx="12" cy="12" r="10.5" strokeWidth="1.3" className={cn("transition-[fill,stroke] duration-[160ms] ease-linear", i < lit ? "fill-foreground" : "fill-background", i <= lit ? "stroke-foreground" : "stroke-[var(--notchset-rule,var(--border))]")} />
              <path d="M7.5 12.2 L10.6 15.2 L16.5 9" fill="none" strokeWidth="1.6" pathLength={1} strokeDasharray={1} className="stroke-background transition-[stroke-dashoffset] delay-[80ms] duration-[260ms] ease-(--notchset-ease-check)" style={{ strokeDashoffset: i < lit ? 0 : 1 }} />
            </svg>
            <span className="flex min-w-0 flex-col gap-[3px] pt-0.5">
              <span className={cn(small, "font-medium tracking-[0.12em] text-muted-foreground")}>{s.kind}</span>
              <span className="text-[13px] font-medium">{s.text}</span>
            </span>
          </li>
        ))}
      </ol>
      {automation.footnote && <span className={cn(small, "truncate tracking-[0.08em] text-muted-foreground")}>{automation.footnote}</span>}
    </div>
  );
}

function FeatureBento({ eyebrow, title, description, review, palette, cost, automation, className, ...props }: FeatureBentoProps) {
  const root = React.useRef<HTMLElement>(null);
  const { t, still } = useTick(root);
  return (
    <section {...NOTCHSET_ROOT} ref={root} data-slot="feature-bento" aria-label={typeof title === "string" ? title : "Features"} className={cn("@container box-border border border-solid border-foreground bg-background text-foreground", className)} {...props}>
      <div className="flex max-w-[600px] flex-col gap-3 px-7 pt-7 pb-[22px] @max-[640px]:px-5">
        {eyebrow && <span className={cn(small, "tracking-[0.14em] text-muted-foreground")}>{eyebrow}</span>}
        <h2 className="m-0 text-[clamp(32px,8cqi,46px)] leading-[0.9] font-extrabold uppercase [font-stretch:62%]">{title}</h2>
        {description && <p className="m-0 text-[15px] leading-[1.5] text-muted-foreground [text-wrap:pretty]">{description}</p>}
      </div>
      {/* Three columns and two 300px rows; one column, tiles at their own height, under 760px. */}
      <div className="grid grid-cols-3 grid-rows-[minmax(300px,auto)_minmax(300px,auto)] border-0 border-t border-solid border-foreground @max-[760px]:grid-cols-1 @max-[760px]:grid-rows-none">
        <Tile tile={review} className="col-span-2 row-start-1 border-0 border-e border-b border-solid border-[var(--notchset-rule,var(--border))] @max-[760px]:col-span-1 @max-[760px]:row-start-auto @max-[760px]:border-e-0">
          <Review review={review} t={t} still={still} />
        </Tile>
        <Tile tile={palette} className="col-start-3 row-span-2 row-start-1 border-0 border-b border-solid border-[var(--notchset-rule,var(--border))] @max-[760px]:col-start-auto @max-[760px]:row-span-1 @max-[760px]:row-start-auto">
          <Palette palette={palette} t={t} still={still} />
        </Tile>
        <Tile tile={cost} className="col-start-1 row-start-2 border-0 border-e border-solid border-[var(--notchset-rule,var(--border))] @max-[760px]:col-start-auto @max-[760px]:row-start-auto @max-[760px]:min-h-[300px] @max-[760px]:border-e-0 @max-[760px]:border-b">
          <Cost cost={cost} />
        </Tile>
        <Tile tile={automation} className="col-start-2 row-start-2 border-0 border-e border-solid border-[var(--notchset-rule,var(--border))] @max-[760px]:col-start-auto @max-[760px]:row-start-auto @max-[760px]:border-e-0">
          <Automation automation={automation} t={t} still={still} />
        </Tile>
      </div>
    </section>
  );
}

export { FeatureBento };
```

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

```tsx
"use client";

import * as React from "react";
import { createPortal } from "react-dom";
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
// ---------------------------------------------------------------------------------------------

/**
 * 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);
      },
    );
  });
}

/** True under prefers-reduced-motion or inside a data-motion="off" scope. Checked when an effect runs. */
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;
  };
}

/**
 * Puts text on the clipboard. Resolves true once it is there and false when the browser refuses (no
 * permission, an insecure page, no clipboard API), so a "copied" state is only ever shown for a copy.
 */
export async function copyText(text: string): Promise<boolean> {
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch {
    return false;
  }
}

/** The glide easing shared by travelling marks (selection blocks, highlights, runs). */
export const GLIDE = "cubic-bezier(.65,0,.35,1)";

/** Base UI's className may be a function of state: merges ours with it in either form. */
export const withClass =
  <S,>(base: string, className: string | ((state: S) => string | undefined) | undefined) =>
  (state: S) =>
    cn(base, typeof className === "function" ? className(state) : className);

/** 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 each step on its own animation frame, in order (the first on the next frame), and returns one
 * cancel that stops whichever frame is pending. A no-op step just waits a frame: frames(set pre, wait,
 * go) lets the browser paint the start state before the transition begins.
 */
export function frames(...steps: (() => void)[]): () => void {
  let id = 0;
  let i = 0;
  const tick = () => {
    steps[i++]?.();
    if (i < steps.length) id = requestAnimationFrame(tick);
  };
  id = requestAnimationFrame(tick);
  return () => cancelAnimationFrame(id);
}
export const wait = () => {};

let idSeq = 0;
/** A unique id for something the user creates (a milestone, a note): random where the browser can, else a counter. */
export const newId = () => (typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `id-${Date.now()}-${++idSeq}`);

/**
 * True while an input method (Japanese, Chinese, Korean…) is composing: Enter, Tab and delimiters
 * then belong to the composition, not to the component. keyCode 229 covers Safari's late flag.
 */
export function composing(e: React.KeyboardEvent | KeyboardEvent) {
  const native = "nativeEvent" in e ? e.nativeEvent : e;
  return native.isComposing || native.keyCode === 229;
}

const hotkeyOwners = new Map<string, number[]>();
let hotkeySeq = 0;
/**
 * A page-wide shortcut ("/" or "mod+k", mod being ⌘ or Ctrl) with a single owner: when several
 * components claim the same key, only the one mounted last answers, so one press never opens two.
 * A plain key is ignored while typing in a field.
 */
export function useHotkey(combo: string | undefined, run: () => void) {
  const latest = useLatest(run);
  React.useEffect(() => {
    if (!combo) return;
    const me = ++hotkeySeq;
    const stack = hotkeyOwners.get(combo) ?? [];
    stack.push(me);
    hotkeyOwners.set(combo, stack);
    const mod = combo.startsWith("mod+");
    const key = (mod ? combo.slice(4) : combo).toLowerCase();
    const onKey = (e: KeyboardEvent) => {
      if (hotkeyOwners.get(combo)?.at(-1) !== me || e.key.toLowerCase() !== key) return;
      if (mod ? !(e.metaKey || e.ctrlKey) : e.metaKey || e.ctrlKey || e.altKey) return;
      if (!mod && (e.target as HTMLElement | null)?.closest("input, textarea, select, [contenteditable=true]")) return;
      e.preventDefault();
      latest.current();
    };
    window.addEventListener("keydown", onKey);
    return () => {
      window.removeEventListener("keydown", onKey);
      const s = hotkeyOwners.get(combo) ?? [];
      s.splice(s.indexOf(me), 1);
    };
  }, [combo, latest]);
}

/** 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;
}

const subscribeNothing = () => () => {};

/**
 * A scope that turns motion or sound off for everything inside it, popups included. A plain
 * data-motion="off" wrapper can't reach a menu that portals to the body; this one also gives its
 * overlays a portal host carrying the same preferences (inside any outer scope's host), so motionOff
 * and the cues see them there too.
 */
export function NotchsetScope({ motion, sound, children, ...props }: React.ComponentProps<"div"> & { motion?: "off"; sound?: "off" }) {
  const outer = React.useContext(NotchsetPortalContext);
  const [host, setHost] = React.useState<HTMLElement | null>(null);
  const client = React.useSyncExternalStore(subscribeNothing, () => true, () => false);
  return (
    <div data-motion={motion} data-sound={sound} {...props}>
      <NotchsetPortalContext value={host ?? outer}>{children}</NotchsetPortalContext>
      {client && createPortal(<div ref={setHost} data-slot="notchset-scope-host" data-motion={motion} data-sound={sound} />, outer ?? document.body)}
    </div>
  );
}

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([" ", "·", "/", ".", ":"]);
const FLIP_CHARS = 24;

export function scramble(text: string, elapsed: number) {
  let out = "";
  for (let i = 0; i < text.length; i++) {
    const ch = text[i]!;
    // Characters past the 24th land with the 24th, so a long label flips as briefly as a short one.
    out += STILL.has(ch) || elapsed >= 110 + Math.min(i, FLIP_CHARS) * 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 + Math.min(text.length, FLIP_CHARS) * 14 + 40;
    const step = (now: number) => {
      const elapsed = now - t0;
      if (elapsed >= end) {
        setFrame(null);
        return;
      }
      setFrame({ for: text, shown: scramble(text, elapsed) });
      raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [text, off, el]);
  // Turned off mid-flip, the label shows its text at once (the cancelled frame never lingers).
  return !off && 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, ref: refProp, ...props }: Omit<React.ComponentProps<"span">, "children"> & { children: string; off?: boolean }) {
  const ref = React.useRef<HTMLSpanElement>(null);
  const refs = useComposedRefs<HTMLSpanElement>(refProp, ref);
  const shown = useGlyphFlip(children, { off, el: ref });
  return (
    <span {...props} ref={refs} data-slot="flip-text" className={cn("relative whitespace-nowrap", className)}>
      <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;
}

/**
 * Digits that roll: each digit is a 0–9 strip; the ones digit moves first and each digit to its left
 * 40ms later, counting digits only (620ms, a small overshoot). `delay` offsets the whole text. Other
 * characters (",", "$", "%", letters) sit still. Screen readers get the plain text. The one
 * implementation behind RollingNumber and the calendar's RollingLabel.
 */
export function DigitRoll({ text, height, delay = 0, className }: { text: string; height: number; delay?: number; className?: string }) {
  const chars = [...text];
  let k = 0;
  const order = chars.map((_, i) => (/\d/.test(chars[chars.length - 1 - i]!) ? k++ : 0)).reverse();
  return (
    <span className={cn("relative inline-flex flex-none overflow-hidden tabular-nums", className)} style={{ height, lineHeight: `${height}px` }}>
      <span className="sr-only">{text}</span>
      <span aria-hidden className="flex">
        {chars.map((d, i) =>
          /\d/.test(d) ? (
            <span
              key={chars.length - i}
              className="flex flex-col transition-transform duration-[620ms] ease-[cubic-bezier(.34,1.15,.5,1)] motion-reduce:transition-none"
              style={{ transform: `translateY(${-Number(d) * height}px)`, transitionDelay: `${delay + order[i]! * 40}ms` }}
            >
              {Array.from({ length: 10 }, (_, n) => (
                <span key={n} style={{ height }}>
                  {n}
                </span>
              ))}
            </span>
          ) : (
            <span key={`s${chars.length - i}`}>{d}</span>
          ),
        )}
      </span>
    </span>
  );
}

/** A number that rolls (DigitRoll at the readout's own size). `delay` offsets it (rows 60ms apart). */
function RollingNumber({ value, height = 32, delay = 0, className }: { value: string | number; height?: number; delay?: number; className?: string }) {
  return <DigitRoll text={String(value)} height={height} delay={delay} {...(className ? { className } : {})} />;
}

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;
```

### lib/beautiful-ui/notchset/button-variants.ts

```tsx
import { cva } from "class-variance-authority";

/*
 * Notchset Button's variants, server-safe like shadcn's: call buttonVariants() from a Server Component
 * (links styled as buttons). The interactive Button lives in a client module.
 */

export const buttonVariants = cva(
  "group/button relative isolate inline-flex shrink-0 cursor-pointer items-center justify-center gap-(--b-gap) rounded-none border-0 font-mono font-medium tracking-[0.06em] whitespace-nowrap uppercase outline-none select-none transition-[background-color,color,translate,border-color] duration-[var(--notchset-fade),var(--notchset-fade),var(--notchset-key-up),var(--notchset-fade)] ease-[var(--notchset-ease-fade),var(--notchset-ease-fade),var(--notchset-ease-key-up),var(--notchset-ease-fade)] disabled:cursor-not-allowed disabled:opacity-40 aria-disabled:cursor-not-allowed aria-disabled:opacity-40 aria-busy:cursor-progress [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--b-ic) aria-pressed:bg-primary aria-pressed:text-primary-foreground data-[mode=armed]:bg-[var(--notchset-signal-text,var(--destructive))] data-[mode=armed]:text-primary-foreground data-[mode=hold]:translate-x-[2px] data-[mode=hold]:translate-y-[2px] data-[mode=hold]:duration-[var(--notchset-key-down)] data-[mode=hold]:ease-[var(--notchset-ease-key-down)]",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-[var(--notchset-primary-hover,color-mix(in_oklab,var(--primary)_86%,var(--background)))] active:bg-[var(--notchset-primary-press,var(--primary))] [--b-tick:var(--notchset-tick-on-primary,color-mix(in_oklab,var(--primary-foreground)_34%,transparent))] [--b-kbd:var(--notchset-kbd-on-primary,color-mix(in_oklab,var(--primary-foreground)_45%,transparent))]",
        destructive:
          "bg-[var(--notchset-signal,var(--destructive))] text-[color:var(--notchset-on-signal,white)] hover:bg-[var(--notchset-signal-hover,var(--destructive))] active:bg-[var(--notchset-signal-press,var(--destructive))] [--b-tick:var(--notchset-tick-on-primary,color-mix(in_oklab,white_34%,transparent))] [--b-kbd:var(--notchset-kbd-on-signal,color-mix(in_oklab,black_40%,transparent))] [--notchset-focus-color:var(--notchset-signal-text,var(--destructive))]",
        outline: "border border-solid border-foreground bg-transparent text-foreground hover:bg-accent active:bg-[var(--notchset-tint-press,var(--accent))]",
        secondary: "bg-secondary text-secondary-foreground hover:bg-[var(--notchset-secondary-hover,var(--secondary))] active:bg-[var(--notchset-secondary-press,var(--secondary))]",
        signal:
          "border border-solid border-[var(--notchset-signal-text,var(--destructive))] bg-background text-[color:var(--notchset-signal-text,var(--destructive))] hover:bg-[var(--notchset-tint,color-mix(in_oklab,var(--destructive)_12%,var(--background)))] [--notchset-button-fill:color-mix(in_oklab,var(--notchset-signal,var(--destructive))_26%,var(--background))] [--notchset-button-meter:var(--notchset-signal,var(--destructive))] [--notchset-focus-color:var(--notchset-signal-text,var(--destructive))]",
        ghost: "bg-transparent text-foreground hover:bg-accent active:bg-[var(--notchset-tint-press,var(--accent))]",
        link: "h-auto min-w-0 justify-start bg-transparent px-0.5 pt-[3px] pb-1.5 text-foreground active:text-[var(--notchset-signal-text,var(--destructive))]",
        plate: "bg-primary text-primary-foreground hover:bg-[var(--notchset-primary-hover,color-mix(in_oklab,var(--primary)_86%,var(--background)))] active:translate-x-[2px] active:translate-y-[2px] active:duration-[var(--notchset-key-down)] active:ease-[var(--notchset-ease-key-down)]",
      },
      size: {
        // The market's heights (shadcn, and Kobra on it): XS 24, S 28, default 32, L 36. Labels are mono caps,
        // which read larger than the 14px sans those libraries use, so 12px (13px on L) matches them optically.
        // Same size names as shadcn, so it stays a drop-in. At least 44px tall on touch screens.
        default: "h-8 min-w-8 px-3 text-[12px] [--b-gap:8px] [--b-ic:16px] [--b-kh:18px] [--b-kfs:10px] pointer-coarse:min-h-11",
        xs: "h-6 min-w-6 px-2 text-[12px] [--b-gap:6px] [--b-ic:14px] [--b-kh:14px] [--b-kfs:10px] pointer-coarse:min-h-11",
        sm: "h-7 min-w-7 px-2.5 text-[12px] [--b-gap:7px] [--b-ic:16px] [--b-kh:16px] [--b-kfs:10px] pointer-coarse:min-h-11",
        lg: "h-9 min-w-9 px-4 text-[13px] [--b-gap:8px] [--b-ic:16px] [--b-kh:20px] [--b-kfs:10px] pointer-coarse:min-h-11",
        icon: "size-8 [--b-ic:16px]",
        "icon-xs": "size-6 [--b-ic:14px]",
        "icon-sm": "size-7 [--b-ic:16px]",
        "icon-lg": "size-9 [--b-ic:16px]",
      },
    },
    // The link's drawn rule lives in the Notchset foundation (a system class, kept out of the utilities).
    // The shared press (a 1px content dip) on every variant but plate, which moves whole onto its plate.
    compoundVariants: [
      { variant: "link", class: `notchset-link` },
      { variant: ["default", "destructive", "outline", "secondary", "signal", "ghost", "link"], class: `notchset-press` },
    ],
    defaultVariants: { variant: "default", size: "default" },
  },
);

export type ButtonVariant = NonNullable<Parameters<typeof buttonVariants>[0]>["variant"];
export type ButtonSize = NonNullable<Parameters<typeof buttonVariants>[0]>["size"];
```

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.
