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

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

shadcn's Alert in Notchset: a 3px rail, a state node, a mono kicker over the title and a budget ruler with its threshold tick; warn is signal, resolved is ink, and changing tone crossfades while the ruler glides.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: none beyond React.
- Files: `components/ui/notchset/alert.tsx`; shared code: `lib/beautiful-ui/notchset/root.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: `Alert`, `AlertAction`, `AlertDescription`, `AlertKicker`, `AlertMeter`, `AlertTitle`, 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 { Alert, AlertAction, AlertKicker, AlertMeter, AlertTitle } from "@/components/ui/notchset/alert";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `Alert` | `alert` | The row. |
| `AlertKicker` | `alert-kicker` | The mono line. |
| `AlertTitle` | `alert-title` | The title. |
| `AlertMeter` | `alert-meter` | The ruler and its reading. |
| `AlertAction` | `alert-action` | The action key. |

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/alert.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
- alert, banner, callout, budget warning, cost alert, resolved notice, shadcn alert, Notchset
- A condition people should act on, with the number that triggered it
- A drop-in for shadcn's Alert (same parts; variant destructive is warn)

### Not when
- Passing confirmations: use Toast
- Blocking decisions: use Dialog

## Mistakes
- Keep the meta reading short: it wraps under the ruler rather than reach the action

## Usage

```tsx
"use client";

import { Alert, AlertAction, AlertKicker, AlertMeter, AlertTitle } from "@/components/ui/notchset/alert";

export function BudgetAlert({ spent, budget, onRaise }: { spent: number; budget: number; onRaise: () => void }) {
  return (
    <Alert tone="warn">
      <AlertKicker>Cost alert · Planner</AlertKicker>
      <AlertTitle>Spend reached {Math.round((spent / budget) * 100)}% of the August budget.</AlertTitle>
      <AlertMeter value={spent} max={budget} threshold={0.8} label="August budget">
        ${spent} OF ${budget} · ALERT AT 80%
      </AlertMeter>
      <AlertAction onClick={onRaise}>Raise to $700</AlertAction>
    </Alert>
  );
}
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `tone` | `"warn" \| "ok" \| "info"` |  | Warn is signal and role=alert; ok and info are ink and role=status. shadcn's variant="destructive" maps to warn. |
| `AlertKicker / AlertTitle / AlertDescription` | `elements` |  | The mono line, the title, body text. |
| `AlertMeter` | `{ value, max, threshold?, label? }` |  | The 160px ruler; threshold is a fraction of max. Children are its reading. |
| `AlertAction` | `button props` |  | The 40px action key at the end of the row. |

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

## 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 |
|---|---|
| Tab | Focus the action |

## Performance

- No JavaScript beyond rendering; tone and ruler changes are CSS transitions.

## Responsive

- Up to 600px; the action drops under the text when the row gets narrow, and the reading wraps under the ruler.

## Motion inventory

| Interaction | What moves |
|---|---|
| Tone | Border, ground, rail, node and kicker crossfade over 300ms; the node's centre blooms |
| Value | The ruler's fill and node glide over 620ms |

## Accessibility contract (preserve when editing)
- role=alert while warning, role=status otherwise (pass role to change it)
- The ruler is a meter with its value and maximum
- Colour is never the only signal: the kicker and title say the state

## Install

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

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

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

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

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

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

## Source (Base UI build)

### components/ui/notchset/alert.tsx

```tsx
/**
 * Alert (Notchset) v1.0.0 · Beautiful UI, by Chimpy (https://x.com/stochastichimp)
 * Docs: https://beautiful-ui.dev/components/alert · Agent prompt: https://beautiful-ui.dev/md/alert.md
 * MIT licensed: free to use, change and share.
 */
import * as React from "react";
import { cn } from "@/lib/utils";
import { NOTCHSET_ROOT } from "@/lib/beautiful-ui/notchset/root";

/*
 * Notchset Alert (handoff 1.6; no primitive, one file for both builds). shadcn's Alert, same parts, in
 * Notchset: a 3px rail on the leading edge, a 13px state node, a mono kicker over the title, and a
 * meta row with a budget ruler (track, fill, node and threshold tick) that wraps rather than touch
 * the action. Warn is signal: tint ground, signal rail and node, role="alert". Ok and info are ink
 * on paper, role="status". Changing tone crossfades its colours over 300ms; the ruler glides 620ms.
 */

type Tone = "warn" | "ok" | "info";

function Alert({
  className,
  tone: toneProp,
  variant,
  children,
  ...props
}: React.ComponentProps<"div"> & {
  /** warn (signal), ok (resolved) or info. */
  tone?: Tone;
  /** shadcn's variant: destructive is warn, default is info. */
  variant?: "default" | "destructive";
}) {
  const tone: Tone = toneProp ?? (variant === "destructive" ? "warn" : "info");
  // The text stacks in one column; actions sit at the end of the row (they wrap under it when narrow).
  const all = React.Children.toArray(children);
  const isAction = (c: React.ReactNode) => React.isValidElement(c) && c.type === AlertAction;
  const actions = all.filter(isAction);
  const text = all.filter((c) => !isAction(c));
  return (
    <div
      {...NOTCHSET_ROOT}
      data-slot="alert"
      data-tone={tone}
      role={tone === "warn" ? "alert" : "status"}
      className={cn(
        "group/alert relative flex w-full max-w-[600px] flex-wrap items-center gap-x-7 gap-y-2.5 border border-solid py-4 ps-5 pe-4 text-start text-foreground",
        "transition-[border-color,background-color] duration-300 ease-linear",
        "border-[var(--notchset-control-edge,var(--input))] bg-background data-[tone=warn]:border-[var(--notchset-signal-text,var(--destructive))] data-[tone=warn]:bg-[var(--notchset-tint,color-mix(in_oklab,var(--destructive)_12%,var(--background)))]",
        className,
      )}
      {...props}
    >
      <span
        aria-hidden
        className="absolute -top-px -bottom-px -start-px w-[3px] bg-foreground transition-[background-color] duration-300 ease-linear group-data-[tone=warn]/alert:bg-[var(--notchset-signal,var(--destructive))]"
      />
      <svg aria-hidden width="13" height="13" viewBox="0 0 13 13" className="flex-none self-center overflow-visible">
        <circle
          cx="6.5"
          cy="6.5"
          r="5"
          fill="none"
          strokeWidth="1"
          className="stroke-foreground transition-[stroke] duration-300 ease-linear group-data-[tone=warn]/alert:stroke-[var(--notchset-signal,var(--destructive))]"
        />
        <circle
          cx="6.5"
          cy="6.5"
          className="fill-foreground [r:2.6px] transition-[r,fill] duration-[400ms,300ms] ease-[cubic-bezier(.34,1.9,.5,1),linear] group-data-[tone=warn]/alert:fill-[var(--notchset-signal,var(--destructive))] group-data-[tone=warn]/alert:[r:2px]"
        />
      </svg>
      <div className="flex min-w-[min(100%,220px)] flex-1 flex-col gap-[5px]">{text}</div>
      {actions}
    </div>
  );
}

/** The mono line above the title: `COST ALERT · PLANNER`. Signal text while warning. */
function AlertKicker({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="alert-kicker"
      className={cn(
        "m-0 font-mono text-[8px] tracking-[0.14em] whitespace-nowrap text-muted-foreground uppercase transition-[color] duration-300 ease-linear group-data-[tone=warn]/alert:text-[var(--notchset-signal-text,var(--destructive))]",
        className,
      )}
      {...props}
    />
  );
}

function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="alert-title" className={cn("font-sans text-[14.5px] leading-[1.3] font-medium", className)} {...props} />;
}

function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="alert-description" className={cn("font-sans text-[13px] leading-[1.5] text-muted-foreground [&_p]:m-0", className)} {...props} />;
}

/**
 * The meta row: a 160px budget ruler and its reading. The ruler shows `value` of `max` with a tick at
 * `threshold` (a fraction of max); the reading wraps under it on narrow widths.
 */
function AlertMeter({
  value,
  max = 100,
  threshold,
  label,
  className,
  children,
  ...props
}: Omit<React.ComponentProps<"div">, "children"> & { value: number; max?: number; threshold?: number; label?: string; children?: React.ReactNode }) {
  const share = Math.max(0, Math.min(1, max > 0 ? value / max : 0));
  const pct = `${(share * 100).toFixed(2)}%`;
  return (
    <div data-slot="alert-meter" className={cn("mt-[5px] flex flex-wrap items-center gap-x-3 gap-y-1.5", className)} {...props}>
      <span
        role="meter"
        aria-label={label ?? "Usage"}
        aria-valuemin={0}
        aria-valuemax={max}
        aria-valuenow={value}
        className="relative h-1.5 w-40 flex-none"
      >
        <span aria-hidden className="absolute inset-x-0 top-[2.5px] h-px bg-[var(--notchset-rule,var(--border))]" />
        <span
          aria-hidden
          className="absolute start-0 top-[2.5px] h-px bg-foreground transition-[width,background-color] duration-[620ms,300ms] ease-[var(--notchset-ease-unfold),linear] group-data-[tone=warn]/alert:bg-[var(--notchset-signal,var(--destructive))]"
          style={{ width: pct }}
        />
        <span
          aria-hidden
          className="absolute top-0 -ms-[3px] size-1.5 rounded-full bg-foreground transition-[inset-inline-start,background-color] duration-[620ms,300ms] ease-[var(--notchset-ease-unfold),linear] group-data-[tone=warn]/alert:bg-[var(--notchset-signal,var(--destructive))]"
          style={{ insetInlineStart: pct }}
        />
        {threshold !== undefined && (
          <span aria-hidden className="absolute top-0 h-1.5 w-px bg-[var(--notchset-control-edge,var(--input))]" style={{ insetInlineStart: `${Math.max(0, Math.min(1, threshold)) * 100}%` }} />
        )}
      </span>
      {children && <span className="font-mono text-[9px] tracking-[0.04em] whitespace-nowrap text-muted-foreground tabular-nums">{children}</span>}
    </div>
  );
}

/** The action key at the end of the alert: 40px, ink border, mono caps. */
function AlertAction({ className, type = "button", ...props }: React.ComponentProps<"button">) {
  return (
    <button
      data-slot="alert-action"
      type={type}
      className={cn(
        `notchset-focus`,
        "h-10 flex-none cursor-pointer self-center max-sm:ms-[41px] border border-solid border-foreground bg-transparent px-3.5 font-mono text-[11px] font-medium tracking-[0.07em] whitespace-nowrap text-foreground uppercase transition-[background-color] duration-[120ms] ease-linear hover:bg-accent pointer-coarse:h-11",
        className,
      )}
      {...props}
    />
  );
}

export { Alert, AlertAction, AlertDescription, AlertKicker, AlertMeter, AlertTitle };
```

### 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.
