# Drop a deal, the quarter answers (Notchset): prompt.md (v2.1.0)

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

A few deals in their stages over the quarter's progress toward quota. Move a card and everything that depends on it answers: the totals roll, the bar moves, and crossing quota sweeps the ring once. A refused save goes home with Retry..

## 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/quarter-drop.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `notchset-foundation`, `company-mark`, `rolling-number`, `tick-bar`.
- Builds: one set of files, the same for every shadcn style.
- Exports to keep: `QuarterDrop`, 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-bezel`, `--ns-canvas`, `--ns-ease`, `--ns-ink`, `--ns-ink-2`, `--ns-ink-3`, `--ns-ok-ink`, `--ns-panel`, `--ns-radius`, `--ns-radius-lg`, `--ns-selected`, `--ns-signal`, `--ns-text-meta`, `--ns-text-small`, `--ns-time-color`, `--ns-time-dialog`, `--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 { QuarterDrop } from "@/components/ui/notchset/blocks/quarter-drop";
```

## Parts
- A single export that takes props.

## Match the original
- Read `components/ui/notchset/blocks/quarter-drop.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
- company-mark (`company-mark`), installed with it: reuse it, don't rebuild it
- rolling-number (`rolling-number`), installed with it: reuse it, don't rebuild it
- tick-bar (`tick-bar`), 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 { QuarterDrop } from "@/components/ui/notchset/blocks/quarter-drop";
import { Switch } from "@/components/ui/notchset/switch";

const k = (v: number) => `$${Math.round(v / 1000)}k`;

export default function QuarterDropDemo() {
  const [fails, setFails] = React.useState(false);
  return (
    <div className="flex w-full max-w-[560px] flex-col gap-2">
      <QuarterDrop
        columns={[
          { key: "proposal", label: "Proposal" },
          { key: "negotiation", label: "Negotiation" },
          { key: "won", label: "Won" },
        ]}
        deals={[
          { id: "q1", company: "Dropbox", color: "#0061ff", value: 48000, column: "negotiation" },
          { id: "q2", company: "Sentry", color: "#362d59", value: 96000, column: "negotiation" },
          { id: "q3", company: "Shopify", color: "#5e8e3e", value: 72000, column: "proposal" },
          { id: "q4", company: "Stripe", color: "#635bff", value: 152000, column: "won" },
        ]}
        wonColumn="won"
        quota={240000}
        wonBefore={64000}
        format={k}
        /* the host's save: resolve false and the card goes home with Retry */
        onMove={() => new Promise<boolean>((r) => setTimeout(() => r(!fails), 380))}
      />
      <label className="flex items-center justify-end gap-2 text-[length:var(--ns-text-small)] text-[var(--ns-ink-3)]">
        Saves fail
        <Switch checked={fails} onCheckedChange={setFails} aria-label="Make saves 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/quarter-drop.json https://notchset.dev/r/switch.json`.

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `columns` | `{ key: string; label: string }[]` |  | Required. |
| `deals` | `DropDeal[]` |  | Required. |
| `wonColumn` | `string` |  | the column that counts as won |
| `quota` | `number` |  | Required. |
| `wonBefore` | `number` |  | what was already won this quarter before these deals |
| `format` | `(v: number) => string` |  | Required. |
| `onMove` | `(id: string, to: string) => Promise<boolean \| void> \| boolean \| void` |  | the host's save; reject (or resolve false) to refuse the move |

Full docs: https://notchset.dev/blocks/quarter-drop

## 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/quarter-drop.json
```

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

## Source

### components/ui/notchset/blocks/quarter-drop.tsx

```tsx
// Notchset quarter-drop 2.1.0 · https://notchset.dev/changelog
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { CompanyMark } from "@/components/ui/notchset/company-mark";
import { RollingNumber } from "@/components/ui/notchset/rolling-number";
import { TickBar } from "@/components/ui/notchset/tick-bar";

/* measured before paint in the browser; on the server (where layout effects warn) an ordinary effect */
const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;

/*
 * Drop a deal, the quarter answers: a few deals in their stages over the quarter's progress toward quota. Move a
 * card (drag it, or focus it and press the arrow keys) and everything that depends on it answers at once: the
 * column totals roll, the quarter's bar moves, and crossing quota is the one moment the ring sweeps the bar. Undo
 * takes it all back. When the host's save fails, the card goes home, the numbers roll back and the reason is said
 * with Retry, never a move that silently didn't happen.
 *
 *   <QuarterDrop columns={[…]} deals={[…]} wonColumn="won" quota={500000} wonBefore={420000} format={money}
 *     onMove={(id, to) => save(id, to)} />
 */
type DropDeal = { id: string; company: string; color: string; value: number; column: string };
type QuarterDropProps = {
  columns: { key: string; label: string }[];
  deals: DropDeal[];
  /** the column that counts as won */
  wonColumn: string;
  quota: number;
  /** what was already won this quarter before these deals */
  wonBefore: number;
  format: (v: number) => string;
  /** the host's save; reject (or resolve false) to refuse the move */
  onMove?: ((id: string, to: string) => Promise<boolean | void> | boolean | void) | undefined;
  className?: string | undefined;
};
/* g: the move's place among every move on the board, so one card's older answer never speaks over another's newer one */
type Note = { kind: "moved" | "failed"; id: string; from: string; to: string; g: number };

function QuarterDrop({ columns, deals: initial, wonColumn, quota, wonBefore, format, onMove, className }: QuarterDropProps) {
  const [deals, setDeals] = React.useState(initial);
  const [note, setNote] = React.useState<Note | null>(null);
  const [drag, setDrag] = React.useState<{ id: string; dx: number; dy: number; x0: number; y0: number; over?: string | undefined } | null>(null);
  const [sweep, setSweep] = React.useState(0);
  const cols = React.useRef<Record<string, HTMLDivElement | null>>({});
  const won = wonBefore + deals.filter((d) => d.column === wonColumn).reduce((a, d) => a + d.value, 0);
  const sum = (k: string) => deals.filter((d) => d.column === k).reduce((a, d) => a + d.value, 0);
  const place = (id: string, to: string) => setDeals((ds) => ds.map((d) => (d.id === id ? { ...d, column: to } : d)));

  /* each deal's moves are kept in order: a refused move sends the card to the newest of its moves that hasn't been
     refused (still saving, or saved), else where it started; an older answer never overrides a newer move */
  const ops = React.useRef<Record<string, { n: number; to: string; state: "saving" | "ok" | "no" }[]>>({});
  const start = React.useRef<Record<string, string>>(Object.fromEntries(initial.map((d) => [d.id, d.column])));
  const seq = React.useRef(0);
  const alive = React.useRef(true);
  React.useEffect(() => {
    alive.current = true;
    return () => void (alive.current = false);
  }, []);
  const move = async (id: string, to: string, quiet = false) => {
    const d = deals.find((x) => x.id === id);
    if (!d || d.column === to) return;
    const from = d.column;
    const list = (ops.current[id] ??= []);
    const op = { n: (list.at(-1)?.n ?? 0) + 1, to, state: "saving" as "saving" | "ok" | "no" };
    list.push(op);
    const g = ++seq.current;
    const wonNow = won + (to === wonColumn ? d.value : 0) - (from === wonColumn ? d.value : 0);
    if (won < quota && wonNow >= quota) setSweep((n) => n + 1);
    place(id, to);
    setNote(quiet ? null : { kind: "moved", id, from, to, g });
    let ok = true;
    try {
      ok = (await onMove?.(id, to)) !== false;
    } catch {
      ok = false;
    }
    op.state = ok ? "ok" : "no";
    /* a saved move is where the host has the deal; older moves no longer matter. Pruned from the history as it is
       now, never from the list this move started with: a late older success must not bring back moves dropped since */
    if (ok) ops.current[id] = (ops.current[id] ?? []).filter((o) => o.n >= op.n || o.state === "saving");
    if (ok) return;
    /* the card goes home a beat after it landed, so you see it refused */
    setTimeout(() => {
      if (!alive.current) return;
      const cur = ops.current[id] ?? [];
      const live = cur.filter((o) => o.state !== "no").at(-1);
      const home = live?.to ?? start.current[id] ?? from;
      place(id, home);
      if (live?.state === "ok") start.current[id] = live.to;
      /* only the newest move speaks; where it ended is where you last asked for it: nothing to retry. A refusal outranks
         a "moved" note on another card, but never replaces another card's newer refusal, nor clears another's note */
      if (cur.at(-1) === op) setNote((n) => (home === to ? (n?.id === id ? null : n) : n && n.kind === "failed" && n.id !== id && n.g > g ? n : { kind: "failed", id, from: home, to, g }));
    }, 260);
  };
  /* the card you're on from the keyboard stays focused when it moves to another column (a new button there) or goes
     home after a refusal; focus you moved to something else stays where you put it */
  const root = React.useRef<HTMLDivElement>(null);
  const focused = React.useRef<string | null>(null);
  useIsomorphicLayoutEffect(() => {
    const id = focused.current;
    const a = document.activeElement;
    if (!id || (a && a !== document.body && !root.current?.contains(a))) return;
    const el = root.current?.querySelector<HTMLElement>(`[data-deal="${CSS.escape(id)}"]`);
    if (el && a !== el) el.focus({ preventScroll: true });
  }, [deals]);
  const step = (id: string, dir: 1 | -1) => {
    const d = deals.find((x) => x.id === id)!;
    const i = columns.findIndex((c) => c.key === d.column) + dir;
    if (columns[i]) void move(id, columns[i]!.key);
  };
  const dropAt = (x: number, y: number) =>
    columns.find((c) => {
      const r = cols.current[c.key]?.getBoundingClientRect();
      return r && x >= r.left && x <= r.right && y >= r.top - 40 && y <= r.bottom + 40;
    })?.key;
  const nd = note ? deals.find((x) => x.id === note.id) : null;
  return (
    <div ref={root} data-slot="quarter-drop" className={cn("flex w-full min-w-0 flex-col gap-4", className)}>
      <div key={sweep} className={cn("flex flex-col gap-1.5 rounded-[var(--ns-radius-lg)] p-1", sweep > 0 && "[animation:ns-ring-sweep_900ms_var(--ns-ease)_1]")} data-ns-moves="">
        <div className="flex items-baseline justify-between gap-3 text-[length:var(--ns-text-small)]">
          <span className="text-[var(--ns-ink-2)]">Won this quarter</span>
          <span className="tabular-nums text-[var(--ns-ink-3)]">
            <RollingNumber value={format(won)} className="font-[number:var(--ns-weight-medium)] text-[var(--ns-ink)]" /> of {format(quota)}
          </span>
        </div>
        <TickBar value={won} max={Math.max(quota * 1.25, won)} target={quota} done={won} label="Won against quota" valueText={`${format(won)} of ${format(quota)}`} />
      </div>
      <div className="grid gap-2" style={{ gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))` }}>
        {columns.map((c) => (
          <div key={c.key} ref={(el) => void (cols.current[c.key] = el)} className={cn("flex min-h-[13rem] min-w-0 flex-col gap-1.5 rounded-[var(--ns-radius-lg)] p-1.5 [transition:background-color_var(--ns-time-color)]", drag?.over === c.key ? "bg-[var(--ns-selected)]" : "bg-[color-mix(in_oklab,var(--ns-well)_80%,transparent)]")}>
            <span className="flex flex-wrap items-baseline gap-x-1.5 px-1 pt-0.5 text-[length:var(--ns-text-meta)]">
              <b className="truncate font-[number:var(--ns-weight-medium)] text-[var(--ns-ink-2)]">{c.label}</b>
              <RollingNumber value={format(sum(c.key))} className={cn("ml-auto shrink-0", c.key === wonColumn ? "text-[var(--ns-ok-ink)]" : "text-[var(--ns-ink-3)]")} />
            </span>
            {deals
              .filter((d) => d.column === c.key)
              .map((d) => {
                const dragging = drag?.id === d.id;
                return (
                  <button
                    key={d.id}
                    type="button"
                    data-deal={d.id}
                    aria-label={`${d.company}, ${format(d.value)}, in ${c.label}. Arrow keys move it`}
                    onFocus={() => void (focused.current = d.id)}
                    onBlur={(e) => void (e.relatedTarget && (focused.current = null))}
                    onKeyDown={(e) => {
                      if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
                        e.preventDefault();
                        step(d.id, e.key === "ArrowRight" ? 1 : -1);
                      }
                    }}
                    onPointerDown={(e) => {
                      e.currentTarget.setPointerCapture?.(e.pointerId);
                      setDrag({ id: d.id, dx: 0, dy: 0, x0: e.clientX, y0: e.clientY });
                    }}
                    onPointerMove={(e) => (dragging && drag ? setDrag({ ...drag, dx: e.clientX - drag.x0, dy: e.clientY - drag.y0, over: dropAt(e.clientX, e.clientY) }) : undefined)}
                    onPointerUp={(e) => {
                      if (!dragging) return;
                      const to = dropAt(e.clientX, e.clientY);
                      setDrag(null);
                      if (to && to !== d.column) void move(d.id, to);
                    }}
                    onPointerCancel={() => setDrag(null)}
                    data-ns-moves=""
                    className={cn(
                      "flex min-w-0 cursor-grab touch-none items-center gap-2 rounded-[var(--ns-radius)] bg-[var(--ns-panel)] px-2.5 py-2 text-left outline-hidden select-none [animation:ns-island-in_var(--ns-time-dialog)_var(--ns-ease)_both] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[var(--ns-signal)]",
                      dragging ? "relative z-10 cursor-grabbing shadow-[0_0_0_1px_var(--ns-signal),0_14px_30px_-12px_rgb(0_0_0/0.4)]" : "shadow-[var(--ns-bezel)]",
                    )}
                    style={dragging && drag ? { transform: `translate(${drag.dx}px, ${drag.dy}px) rotate(1.5deg)` } : undefined}
                  >
                    <CompanyMark name={d.company} color={d.color} size={16} className="shrink-0 self-start max-[600px]:hidden" />
                    {/* the name over the value, so a narrow column still reads both whole */}
                    <span className="flex min-w-0 flex-1 flex-col">
                      <span className="truncate text-[length:var(--ns-text-small)] font-[number:var(--ns-weight-medium)] text-[var(--ns-ink)]">{d.company}</span>
                      <span className="text-[length:var(--ns-text-meta)] text-[var(--ns-ink-2)] tabular-nums">{format(d.value)}</span>
                    </span>
                  </button>
                );
              })}
          </div>
        ))}
      </div>
      <div aria-live="polite" className="min-h-9">
        {note && nd && (
          <div data-ns-moves="" className="flex items-center gap-3 rounded-[var(--ns-radius)] bg-[var(--ns-ink)] px-3 py-2 text-[length:var(--ns-text-small)] text-[var(--ns-canvas)] [animation:ns-island-in_var(--ns-time-dialog)_var(--ns-ease)_both]">
            <span className="min-w-0 flex-1">
              {note.kind === "moved" ? `${nd.company} moved to ${columns.find((c) => c.key === note.to)?.label}` : `Couldn't move ${nd.company}. The save was refused; nothing changed.`}
            </span>
            <button
              type="button"
              onClick={() => (note.kind === "moved" ? void move(note.id, note.from, true) : void move(note.id, note.to))}
              className="shrink-0 cursor-pointer rounded-[6px] px-2 py-0.5 font-[number:var(--ns-weight-medium)] outline-hidden hover:bg-[color-mix(in_oklab,var(--ns-canvas)_14%,transparent)] focus-visible:outline-2 focus-visible:outline-[var(--ns-canvas)]"
            >
              {note.kind === "moved" ? "Undo" : "Retry"}
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

export { QuarterDrop, type DropDeal, type QuarterDropProps };
```

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.
