# Sign to close (Notchset): prompt.md (v2.0.0)

- id: `sign-to-close` · version 2.0.0 · block · free
- category: Marketing
- build: one set of files for every shadcn style
- install: `npx shadcn@latest add https://notchset.dev/r/sign-to-close.json`
- npm dependencies: none
- registry dependencies: utils, https://notchset.dev/r/notchset-foundation.json, @notchset/button, @notchset/company-mark, @notchset/status-pill
- docs: https://notchset.dev/blocks/sign-to-close
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

A small order form that ends in a signature: draw it, with Undo and Clear, or type your name. Signing flips the deal to Won and the Signature traces the card once; a refused signature keeps the ink and offers Retry. A line marked one-time is totalled apart: recurring per year, one-time and the first year..

## Build it
- Stack: React 18.3 or 19, TypeScript, Tailwind CSS v4 utility classes on the foundation's roles, a components.json (imports use its aliases).
- Packages: none beyond React.
- Files: `components/ui/notchset/blocks/sign-to-close.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `notchset-foundation`, `button`, `company-mark`, `status-pill`.
- Builds: one set of files, the same for every shadcn style.
- Exports to keep: `SignToClose`, and every exported type.
- CSS: the install adds the notchset foundation (its roles, keyframes and motion switch, reading your shadcn tokens for colour) 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.
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- This item reads `--ns-bad-ink`, `--ns-control`, `--ns-ease`, `--ns-ease-panel`, `--ns-font-display`, `--ns-font-script`, `--ns-ink`, `--ns-ink-2`, `--ns-ink-3`, `--ns-line`, `--ns-line-soft`, `--ns-ok-ink`, `--ns-panel`, `--ns-radius-lg`, `--ns-radius-xl`, `--ns-shadow-float`, `--ns-signal`, `--ns-text-meta`, `--ns-text-small`, `--ns-text-strong`, `--ns-text-title`, `--ns-time-dialog`, `--ns-tracking-display`, `--ns-weight-medium`, `--ns-well`.
- Retune it (on any ancestor) with the foundation's roles and hooks: `--ns-bad`, `--ns-bad-ink`, `--ns-band`, `--ns-band-edge`, `--ns-band-grade`, `--ns-band-optimal`, `--ns-beat`, `--ns-bezel`, `--ns-canvas`, `--ns-cat-1`, `--ns-cat-2`, `--ns-cat-3`, `--ns-cat-4`, `--ns-cat-5`, `--ns-cat-6`, `--ns-check`, `--ns-chip`, `--ns-control`, `--ns-control-pad`, `--ns-control-sm`, `--ns-deep`, `--ns-deep-ink`, `--ns-deep-ink-2`, `--ns-deep-line`, `--ns-dialog`, `--ns-dialog-lg`, `--ns-dialog-sm`, `--ns-ease`, `--ns-ease-in`, `--ns-ease-panel`, `--ns-ease-pop`, `--ns-field`, `--ns-flood-arc`, `--ns-flood-aurora`, `--ns-flood-behind-1`, `--ns-flood-behind-2`, `--ns-flood-bloom`, `--ns-flood-deep`, `--ns-flood-dial-bloom`, `--ns-flood-glass`, `--ns-flood-grain`, `--ns-flood-image`, `--ns-flood-ink`, `--ns-flood-ink-low`, `--ns-flood-light`, `--ns-flood-mid`, `--ns-flood-pale`, `--ns-flood-pane`, `--ns-flood-pane-edge`, `--ns-flood-pill-ink`, `--ns-flood-ribbon`, `--ns-flood-ribbon-core`, `--ns-flood-shadow`, `--ns-flood-sheen`, `--ns-flood-wash`, `--ns-font-body`, `--ns-font-display`, `--ns-font-mono`, `--ns-font-script`, `--ns-glass`, `--ns-glass-blur`, `--ns-glass-shadow`, `--ns-glow`, `--ns-grain`, `--ns-gutter`, `--ns-header`, `--ns-heat`, `--ns-hover`, `--ns-ink`, `--ns-ink-2`, `--ns-ink-3`, `--ns-ink-4`, `--ns-leading`, `--ns-lift`, `--ns-light-1`, `--ns-light-2`, `--ns-light-3`, `--ns-light-spread`, `--ns-light-strength`, `--ns-line`, `--ns-line-soft`, `--ns-lit-edge`, `--ns-menu-dy`, `--ns-menu-row`, `--ns-mesh`, `--ns-mesh-frost`, `--ns-ok`, `--ns-ok-ink`, `--ns-orb-body`, `--ns-orb-cool`, `--ns-orb-core`, `--ns-orb-dawn`, `--ns-orb-deep`, `--ns-orb-edge`, `--ns-orb-grain`, `--ns-orb-ground`, `--ns-orb-halo`, `--ns-orb-inner`, `--ns-orb-key`, `--ns-orb-lift`, `--ns-orb-limb`, `--ns-orb-limb-from`, `--ns-orb-rim`, `--ns-orb-skin-hi`, `--ns-orb-skin-lo`, `--ns-orb-skin-mid`, `--ns-orb-warm`, `--ns-panel`, `--ns-panel-w`, `--ns-prism-edge`, `--ns-prism-glint`, `--ns-prism-sheen`, `--ns-prism-tint`, `--ns-pulse`, `--ns-radius`, `--ns-radius-card`, `--ns-radius-full`, `--ns-radius-lg`, `--ns-radius-sm`, `--ns-radius-xl`, `--ns-raised`, `--ns-range`, `--ns-row`, `--ns-row-compact`, `--ns-segment`, `--ns-selected`, `--ns-series`, `--ns-series-core`, `--ns-series-lilac`, `--ns-series-tint`, `--ns-shadow-dialog`, `--ns-shadow-float`, `--ns-shadow-panel`, `--ns-sheet`, `--ns-sheet-bottom`, `--ns-side`, `--ns-signal`, `--ns-sleep-awake`, `--ns-sleep-deep`, `--ns-sleep-light`, `--ns-sleep-rem`, `--ns-space-1`, `--ns-space-2`, `--ns-space-3`, `--ns-space-4`, `--ns-space-5`, `--ns-space-6`, `--ns-space-8`, `--ns-space-section`, `--ns-stack-on`, `--ns-standing-critical`, `--ns-standing-in`, `--ns-standing-out`, `--ns-state-doing`, `--ns-state-done`, `--ns-state-review`, `--ns-state-todo`, `--ns-switch-h`, `--ns-switch-w`, `--ns-text-body`, `--ns-text-control`, `--ns-text-display`, `--ns-text-figure`, `--ns-text-headline`, `--ns-text-hero`, `--ns-text-lede`, `--ns-text-meta`, `--ns-text-page`, `--ns-text-record`, `--ns-text-small`, `--ns-text-strong`, `--ns-text-sub`, `--ns-text-title`, `--ns-tile-shadow`, `--ns-time-born`, `--ns-time-color`, `--ns-time-dialog`, `--ns-time-menu`, `--ns-time-page`, `--ns-time-panel`, `--ns-time-toast`, `--ns-tint`, `--ns-tint-ink`, `--ns-toast-bottom`, `--ns-toast-left`, `--ns-touch`, `--ns-tracking-display`, `--ns-tracking-headline`, `--ns-warn`, `--ns-warn-ink`, `--ns-wash-clay`, `--ns-wash-dusk`, `--ns-wash-graphite`, `--ns-wash-ink`, `--ns-wash-sage`, `--ns-wash-sand`, `--ns-weight-light`, `--ns-weight-medium`, `--ns-weight-regular`, `--ns-weight-strong`, `--ns-well`.
- Never add Tailwind colour classes inside the component.

```tsx
import { SignToClose } from "@/components/ui/notchset/blocks/sign-to-close";
```

## Parts
- A single export that takes props.

## Match the original
- Read `components/ui/notchset/blocks/sign-to-close.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

### Not when

## Built from
- button (`button`), installed with it: reuse it, don't rebuild it
- company-mark (`company-mark`), installed with it: reuse it, don't rebuild it
- status-pill (`status-pill`), installed with it: reuse it, don't rebuild it

## Mistakes
- Recreating the component instead of installing it

## Usage

```tsx
"use client";
import * as React from "react";
import { SignToClose } from "@/components/ui/notchset/blocks/sign-to-close";
import { Switch } from "@/components/ui/notchset/switch";

const money = (v: number) => `$${Math.round(v).toLocaleString("en-US")}`;

export default function SignToCloseDemo() {
  const [fails, setFails] = React.useState(false);
  return (
    <div className="flex w-full max-w-[420px] flex-col gap-2">
      <SignToClose
        company="Stripe"
        color="#635bff"
        title="Expansion · EU region"
        stage={{ label: "Proposal", color: "#7c3aed" }}
        lines={[
          { label: "Platform · 120 seats", amount: 129600 },
          { label: "EU data residency", amount: 18000 },
          { label: "Onboarding", amount: 4400, once: true },
        ]}
        signer="Ana Ruiz"
        format={money}
        /* resolve false and the ink stays, the pill doesn't move, and Retry is offered */
        onSign={() => new Promise<boolean>((r) => setTimeout(() => r(!fails), 520))}
        replay
      />
      <label className="flex items-center justify-end gap-2 text-[length:var(--ns-text-small)] text-[var(--ns-ink-3)]">
        Signing fails
        <Switch checked={fails} onCheckedChange={setFails} aria-label="Make signing fail" />
      </label>
    </div>
  );
}
```

This example also uses `switch` (https://notchset.dev/components/switch). Installing this item doesn't bring them; to paste the example as it is: `npx shadcn@latest add https://notchset.dev/r/sign-to-close.json https://notchset.dev/r/switch.json`.

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `company` | `string` |  | Required. |
| `color` | `string` |  | Required. |
| `title` | `string` |  | Required. |
| `stage` | `{ label: string; color: string }` |  | the stage the deal sits in until it's signed |
| `lines` | `Line[]` |  | Required. |
| `signer` | `string` |  | who signs; the typed path starts from this name |
| `format` | `(v: number) => string` |  | Required. |
| `onSign` | `(sig: Signature) => Promise<boolean \| void> \| boolean \| void` |  | the host's save; reject (or resolve false) to refuse it |
| `dealId` | `string` |  | which deal this is; given another, the form starts open and the first deal's answer lands nowhere (left out, key the form by the deal) |
| `replay` | `boolean` | `false` | a demo's "Sign again", which clears the signature and reopens the deal |

Full docs: https://notchset.dev/blocks/sign-to-close

## Customising
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- Dark mode follows the `.dark` class or `data-theme="dark"` on an ancestor (the shadcn and next-themes conventions).
- Update later: Commit or stash local changes first (or copy the files), so the update can be undone. Read https://notchset.dev/r/changelog.json for the new versions: a major version, an entry marked breaking or a merge says what to change. Then re-run the install with `--overwrite`, on a branch. It replaces the component's files, the shared helper files they import, and the item's CSS rules in the global stylesheet, so review the whole diff (components, shared files, CSS), re-apply your own edits, and run the app's typecheck, tests and build.

## Accessibility contract (preserve when editing)

## Install

```bash
npx shadcn@latest add https://notchset.dev/r/sign-to-close.json
```

Or add the `@notchset` registry to components.json and run `npx shadcn@latest add @notchset/sign-to-close`. Every components.json style gets the same files.

## Source

### components/ui/notchset/blocks/sign-to-close.tsx

```tsx
// Notchset sign-to-close 2.0.0 · https://notchset.dev/changelog
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/notchset/button";
import { CompanyMark } from "@/components/ui/notchset/company-mark";
import { StatusPill } from "@/components/ui/notchset/status-pill";

/*
 * Sign to close: a small order form that ends in a signature. Draw on the line with a finger, pen or mouse (Undo
 * takes the last stroke back, Clear starts over), or type your name and it's set in a script face, so the keyboard
 * signs too. Sign records it: the deal's pill flips to Won and the Signature traces the card's edge once, the one
 * moment the region marks. When the host refuses, the ink stays, the pill doesn't move and the reason is said with
 * Retry.
 *
 *   <SignToClose company="Stripe" color="#635bff" title="Expansion · EU region" stage={{ label: "Proposal", color: "#7c3aed" }}
 *     lines={[{ label: "Platform · 120 seats", amount: 129600 }, { label: "Onboarding", amount: 4400, once: true }]}
 *     signer="Ana Ruiz" format={money} onSign={(sig) => save(sig)} />
 *
 * Lines recur yearly unless marked `once`. With no one-time line the total is the yearly figure ("Total, per year");
 * with one, the form says what recurs, what is charged once and the first year's total, and the signed line says
 * the first year is what the quarter books, so a one-time fee is never shown as yearly value.
 */
type Line = { label: string; amount: number; /** charged once, not every year */ once?: boolean | undefined };
type Signature = { kind: "drawn"; strokes: [number, number][][] } | { kind: "typed"; name: string };
type SignToCloseProps = {
  company: string;
  color: string;
  title: string;
  /** the stage the deal sits in until it's signed */
  stage: { label: string; color: string };
  lines: Line[];
  /** who signs; the typed path starts from this name */
  signer: string;
  format: (v: number) => string;
  /** the host's save; reject (or resolve false) to refuse it */
  onSign?: ((sig: Signature) => Promise<boolean | void> | boolean | void) | undefined;
  /** which deal this is; given another, the form starts open and the first deal's answer lands nowhere (left out, key the form by the deal) */
  dealId?: string | undefined;
  /** a demo's "Sign again", which clears the signature and reopens the deal */
  replay?: boolean | undefined;
  className?: string | undefined;
};

const PAD_H = 96;

/** a stroke as a smooth path: straight to the first midpoint, then quadratic curves through each point */
function path(s: [number, number][]) {
  if (s.length === 1) return `M${s[0]![0]} ${s[0]![1]}l0.1 0`;
  let d = `M${s[0]![0]} ${s[0]![1]}`;
  for (let i = 1; i < s.length - 1; i++) {
    const [x, y] = s[i]!, [nx, ny] = s[i + 1]!;
    d += `Q${x} ${y} ${(x + nx) / 2} ${(y + ny) / 2}`;
  }
  const [lx, ly] = s[s.length - 1]!;
  return `${d}L${lx} ${ly}`;
}

function SignToClose({ company, color, title, stage, lines, signer, format, onSign, dealId, replay = false, className }: SignToCloseProps) {
  const [strokes, setStrokes] = React.useState<[number, number][][]>([]);
  const [typing, setTyping] = React.useState(false);
  const [typed, setTyped] = React.useState(signer);
  const [state, setState] = React.useState<"open" | "busy" | "won" | "failed">("open");
  const [traced, setTraced] = React.useState(0);
  /* a generation per deal: a save still on its way for the deal before lands nowhere */
  const [deal, setDeal] = React.useState(dealId);
  const generation = React.useRef(0);
  if (deal !== dealId) {
    setDeal(dealId);
    setStrokes([]);
    setTyping(false);
    setTyped(signer);
    setState("open");
    setTraced(0);
  }
  React.useEffect(() => {
    generation.current += 1;
  }, [dealId]);
  /* the one pointer drawing, if any: a second finger or pen draws nothing, and Clear, Undo, Type and Sign end the
     stroke in progress, so a later move can't add ink to a cleared pad or to a signature already sent */
  const pen = React.useRef<number | null>(null);
  const end = () => void (pen.current = null);
  const total = lines.reduce((a, l) => a + l.amount, 0);
  const once = lines.filter((l) => l.once).reduce((a, l) => a + l.amount, 0), yearly = total - once;
  const mixed = lines.some((l) => l.once);
  const won = state === "won";
  const ready = typing ? typed.trim().length > 1 : strokes.length > 0;
  const locked = won || state === "busy";

  const at = (e: React.PointerEvent<SVGSVGElement>): [number, number] => {
    const r = e.currentTarget.getBoundingClientRect();
    return [Math.round(e.clientX - r.left), Math.round(e.clientY - r.top)];
  };
  const sign = async () => {
    if (!ready || locked) return;
    end();
    setState("busy");
    const mine = generation.current;
    let ok = true;
    try {
      ok = (await onSign?.(typing ? { kind: "typed", name: typed.trim() } : { kind: "drawn", strokes })) !== false;
    } catch {
      ok = false;
    }
    if (generation.current !== mine) return;
    setState(ok ? "won" : "failed");
    if (ok) setTraced((n) => n + 1);
  };
  const reset = () => {
    setStrokes([]);
    setState("open");
  };

  return (
    <div data-slot="sign-to-close" className={cn("relative flex w-full min-w-0 flex-col gap-4 rounded-[var(--ns-radius-xl)] bg-[var(--ns-panel)] p-5 shadow-[var(--ns-shadow-float)]", className)}>
      {traced > 0 && (
        <svg key={traced} aria-hidden className="pointer-events-none absolute inset-0 size-full overflow-visible">
          <rect
            width="100%"
            height="100%"
            rx="16"
            pathLength={1}
            data-ns-moves=""
            className="fill-none stroke-[var(--ns-signal)] [stroke-dasharray:1] [stroke-width:2] opacity-0 [animation:ns-ring-trace_1100ms_var(--ns-ease-panel)_both]"
          />
        </svg>
      )}
      <div className="flex items-center gap-3">
        <CompanyMark name={company} color={color} size={28} className="shrink-0" />
        <div className="flex min-w-0 flex-1 flex-col">
          <span className="truncate text-[length:var(--ns-text-strong)] font-[number:var(--ns-weight-medium)] text-[var(--ns-ink)]">{company}</span>
          <span className="truncate text-[length:var(--ns-text-small)] text-[var(--ns-ink-3)]">{title}</span>
        </div>
        <span key={won ? "won" : "open"} data-ns-moves="" className="[animation:ns-island-in_var(--ns-time-dialog)_var(--ns-ease)_both]">
          {won ? <StatusPill tone="ok">Won</StatusPill> : <StatusPill color={stage.color}>{stage.label}</StatusPill>}
        </span>
      </div>

      <dl className="flex flex-col text-[length:var(--ns-text-small)]">
        {lines.map((l) => (
          <div key={l.label} className="flex items-baseline gap-3 border-b border-[var(--ns-line-soft)] py-1.5">
            <dt className="min-w-0 flex-1 truncate text-[var(--ns-ink-2)]">{l.label}</dt>
            <dd className="tabular-nums text-[var(--ns-ink)]">
              {format(l.amount)}
              {l.once && <span className="text-[var(--ns-ink-3)]"> once</span>}
            </dd>
          </div>
        ))}
        {mixed &&
          ([["Recurring, per year", yearly], ["One-time", once]] as const).map(([k, v]) => (
            <div key={k} className="flex items-baseline gap-3 pt-1.5 text-[var(--ns-ink-2)]">
              <dt className="flex-1">{k}</dt>
              <dd className="tabular-nums">{format(v)}</dd>
            </div>
          ))}
        <div className="flex items-baseline gap-3 pt-2">
          <dt className="flex-1 font-[number:var(--ns-weight-medium)] text-[var(--ns-ink)]">{mixed ? "First year" : "Total, per year"}</dt>
          <dd className="font-[family-name:var(--ns-font-display)] text-[length:var(--ns-text-title)] font-[number:var(--ns-weight-medium)] tracking-[var(--ns-tracking-display)] tabular-nums text-[var(--ns-ink)]">{format(total)}</dd>
        </div>
      </dl>

      <div className="flex flex-col gap-1.5">
        <span className="truncate text-[length:var(--ns-text-meta)] text-[var(--ns-ink-3)]">
          {won ? `Signed by ${typing ? typed.trim() : signer}, today` : `${signer} signs for ${company}`}
        </span>
        <div className="relative rounded-[var(--ns-radius-lg)] bg-[var(--ns-well)]" style={{ height: PAD_H }}>
          {typing ? (
            <input
              aria-label="Type your name to sign"
              value={typed}
              readOnly={locked}
              onChange={(e) => setTyped(e.target.value)}
              /* Enter that confirms an input method's candidate is the name still being written, not a signature */
              onKeyDown={(e) => e.key === "Enter" && !e.nativeEvent.isComposing && e.keyCode !== 229 && void sign()}
              className="absolute inset-x-4 bottom-5 bg-transparent font-[family-name:var(--ns-font-script)] text-[34px] leading-none text-[var(--ns-ink)] outline-hidden"
            />
          ) : (
            <svg
              role="img"
              aria-label={strokes.length ? `Your signature, ${strokes.length} ${strokes.length === 1 ? "stroke" : "strokes"}` : "Signature pad. Draw here, or type your name instead"}
              className={cn("absolute inset-0 size-full touch-none", !locked && "cursor-crosshair")}
              onPointerDown={(e) => {
                if (locked || pen.current != null) return;
                e.currentTarget.setPointerCapture?.(e.pointerId);
                pen.current = e.pointerId;
                const p = at(e);
                setStrokes((s) => [...s, [p]]);
                if (state === "failed") setState("open");
              }}
              onPointerMove={(e) => {
                if (pen.current !== e.pointerId || locked) return;
                const p = at(e);
                setStrokes((s) => (s.length ? [...s.slice(0, -1), [...s[s.length - 1]!, p]] : s));
              }}
              onPointerUp={(e) => void (pen.current === e.pointerId && end())}
              onPointerCancel={(e) => void (pen.current === e.pointerId && end())}
              onLostPointerCapture={(e) => void (pen.current === e.pointerId && end())}
            >
              {strokes.map((s, i) => (
                <path key={i} d={path(s)} className="fill-none stroke-[var(--ns-ink)] [stroke-linecap:round] [stroke-linejoin:round] [stroke-width:2.4]" />
              ))}
            </svg>
          )}
          <span aria-hidden className="pointer-events-none absolute inset-x-4 bottom-4 flex items-end gap-2 border-b border-dashed border-[var(--ns-line)] pb-0.5 text-[length:var(--ns-text-meta)] text-[var(--ns-ink-3)]">
            ×{!typing && !strokes.length && <span className="pb-px">Sign here</span>}
          </span>
        </div>
        <div className="flex items-center gap-1 text-[length:var(--ns-text-meta)] text-[var(--ns-ink-3)]">
          <span className="flex-1" />
          {!won && !typing && (
            <>
              <Button size="sm" variant="ghost" disabled={!strokes.length || locked} onClick={() => (end(), setStrokes((s) => s.slice(0, -1)))}>Undo</Button>
              <Button size="sm" variant="ghost" disabled={!strokes.length || locked} onClick={() => (end(), setStrokes([]))}>Clear</Button>
            </>
          )}
          {!won && (
            <Button size="sm" variant="ghost" disabled={locked} onClick={() => (end(), setTyping((t) => !t))}>
              {typing ? "Draw instead" : "Type instead"}
            </Button>
          )}
        </div>
      </div>

      <div aria-live="polite" className="flex min-h-[var(--ns-control)] items-center justify-end gap-3">
        {state === "failed" && (
          <span data-ns-moves="" className="min-w-0 flex-1 text-[length:var(--ns-text-small)] text-[var(--ns-bad-ink)] [animation:ns-island-in_var(--ns-time-dialog)_var(--ns-ease)_both]">
            Couldn&rsquo;t record the signature. Nothing was sent; your signature is kept.
          </span>
        )}
        {won ? (
          <>
            <span data-ns-moves="" className="min-w-0 flex-1 text-[length:var(--ns-text-small)] text-[var(--ns-ok-ink)] [animation:ns-island-in_var(--ns-time-dialog)_var(--ns-ease)_both]">
              {mixed ? `Won. ${format(total)} first-year value (${format(yearly)} a year, ${format(once)} once) added to the quarter.` : `Won. ${format(total)} a year added to the quarter.`}
            </span>
            {replay && <Button variant="ghost" onClick={reset}>Sign again</Button>}
          </>
        ) : (
          <Button variant="primary" disabled={!ready} busy={state === "busy"} onClick={() => void sign()}>
            {state === "busy" ? "Signing…" : state === "failed" ? "Retry" : "Sign and close"}
          </Button>
        )}
      </div>
    </div>
  );
}

export { SignToClose, type Signature, type SignToCloseProps };
```

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