# Bursar — Animated App UI Block for React

> Billing settings page as one block: a plan card with a seat bar and an inline plan switcher (monthly/annual toggle, Tally-rolled prices, live proration line, async onChangePlan), a retention-flow cancel dialog (reason → discount offer → confirm), usage meters that spring to width with warn/overage states and drawn 30-day sparklines, a CSS payment card that flips in 3D to an edit form with grouped card-number entry and inline billing-contact editing, and an invoice table with expanding line items, per-row download, failed-payment retry and cascading Load more. Presentation only — use a PCI-compliant card element in production.

- Page: https://crucibleui.com/components/app/bursar
- Category: [app](https://crucibleui.com/components/app)
- Tier: pro (one-time purchase, see https://crucibleui.com/pricing)
- Engine: motion
- npm dependencies: motion

## Install

```bash
npx shadcn@latest add @crucible/bursar
```

Pro install requires adding the token-bearing `@crucible` registry to components.json (issued at https://crucibleui.com/account after purchase).

Import from `@/components/crucible/bursar`.

## Props (`BursarProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `plan?` | `BursarSubscription` | — | The current subscription. Uncontrolled after mount (re-syncs when the object changes). |
| `plans?` | `BursarPlan[]` | — | Plans offered by the inline switcher (2–4 recommended). |
| `usage?` | `BursarMeter[]` | — | Usage meters for the current period (3 recommended). A meter with `id: "seats"` tracks the subscription's seats and plan limit. |
| `paymentMethod?` | `BursarPaymentMethod` | — | Card on file plus billing contact. |
| `invoices?` | `BursarInvoice[]` | — | Invoice history, newest first. |
| `pageSize?` | `number` | `5` | Invoices shown before "Load more" (and per page after). |
| `cancelOffer?` | `BursarCancelOffer \| null` | `30% off for 3 months` | Retention offer in the cancel flow; `null` skips the offer step. |
| `onChangePlan?` | `(plan: BursarPlan, cycle: BursarCycle) => void \| Promise<unknown>` | — | Called on "Switch to …". Resolve to apply, reject (with an Error message) to show it inline. |
| `onCancel?` | `(reason: string, acceptedOffer: boolean) => void \| Promise<unknown>` | — | Called with the chosen reason and whether the retention offer was accepted. Async-aware. |
| `onUpdatePayment?` | `(update: BursarPaymentUpdate) => void \| Promise<unknown>` | — | Called when the card or the billing contact is saved. Async-aware. |
| `onDownload?` | `(invoice: BursarInvoice) => void \| Promise<unknown>` | — | Called by an invoice's Download button. Async-aware (spinner → check). |
| `onRetry?` | `(invoice: BursarInvoice) => void \| Promise<unknown>` | — | Called by "Retry payment" on a failed invoice. Resolve → paid; reject → inline error. |
| `currency?` | `string` | `"USD"` | ISO 4217 currency for every amount. |
| `locale?` | `string` | `"en-US"` | BCP 47 locale for numbers and dates. |
| `accentColor?` | `string` | `"#86efac"` | Signature accent (primary buttons, focus rings, bars). Text on accent fills auto-picks black/white. |
| `container?` | `HTMLElement \| null` | ``document.body` (full-viewport modal)` | Portal target for the cancel dialog. |
| `ref?` | `React.Ref<BursarHandle>` | — | Imperative handle (`replay`, `openPlans`). React 19 ref-as-prop. |
| `className?` | `string` | — | Extra classes for the root. |

## Usage example

```tsx
"use client";

import * as React from "react";

import { Bursar } from "@/components/crucible/bursar";

export default function BursarDemo() {
  return (
    <div className="relative isolate flex h-[640px] min-h-[360px] w-full flex-col overflow-hidden rounded-xl bg-neutral-950">
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 -z-10"
        style={{
          background:
            "radial-gradient(60% 40% at 50% 0%, rgba(134,239,172,0.07), transparent 70%), radial-gradient(40% 40% at 50% 100%, rgba(255,255,255,0.03), transparent 70%)",
        }}
      />
      <div className="relative z-10 min-h-0 flex-1 overflow-y-auto overscroll-contain [scrollbar-width:thin]">
        <div className="mx-auto w-full max-w-5xl px-3 py-5 sm:px-6">
          <p className="mb-4 text-xs text-white/45">Change plan — flip the card to edit.</p>
          <Bursar />
        </div>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.
