{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "invoice-1",
  "title": "Invoice 1: invoice you can pay",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "An invoice you can read, print and pay: parties, dates and line items, totals worked out from the lines, a status from the dates (paid, due in N days, overdue by N days) and a Pay now that handles waiting and failure.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/invoice-1/invoice-1.tsx",
      "content": "// Ballmac UI: Invoice 1. https://ui.ballmac.com/blocks/invoice-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { CircleAlert, CircleCheck, Clock, CreditCard, Download, Loader2, Printer } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Invoice1Line = { id: string; description: string; detail?: string; quantity: number; unitPrice: number }\ntype Invoice1Party = { name: string; lines: string[] }\n\ntype Invoice1Data = {\n  number: string\n  /** ISO dates. */\n  issued: string\n  due: string\n  /** Set when it has been paid. */\n  paidOn?: string\n  from: Invoice1Party\n  to: Invoice1Party\n  lines: Invoice1Line[]\n  /** Percent off the subtotal, 0 to 100. */\n  discountPercent?: number\n  /** Tax rate in percent. */\n  taxPercent?: number\n  note?: string\n  /** How to pay by transfer, shown when unpaid. */\n  paymentDetails?: { label: string; value: string }[]\n}\n\ntype Invoice1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    invoice?: Invoice1Data\n    /** An ISO date for \"today\", used to say whether it is overdue. The block never reads the clock. */\n    today?: string\n    currency?: string\n    locale?: string\n    /** Called by Pay now. Return a promise; reject to show its message. Leave out to hide the button. */\n    onPay?: () => void | Promise<void>\n    /** Called by Download. Leave out to hide the button. */\n    onDownload?: () => void\n  }\n\nconst sample: Invoice1Data = {\n  number: \"INV-2041\",\n  issued: \"2026-10-01\",\n  due: \"2026-10-15\",\n  from: { name: \"Acme Studio Ltd\", lines: [\"Rua da Prata 120\", \"1100-420 Lisboa, Portugal\", \"VAT PT 509 123 456\"] },\n  to: { name: \"Northwind Logistics\", lines: [\"Attn: Elena Marsh\", \"88 Orchard St\", \"New York, NY 10002\"] },\n  lines: [\n    { id: \"1\", description: \"Product design sprint\", detail: \"Two weeks, three designers\", quantity: 1, unitPrice: 9600 },\n    { id: \"2\", description: \"Design system audit\", detail: \"Components, tokens and accessibility\", quantity: 1, unitPrice: 3200 },\n    { id: \"3\", description: \"Workshop days\", detail: \"On site, including travel\", quantity: 2, unitPrice: 1450 },\n    { id: \"4\", description: \"Prototype hosting\", detail: \"Per month\", quantity: 3, unitPrice: 40 },\n  ],\n  discountPercent: 5,\n  taxPercent: 0,\n  note: \"Thank you for working with us. Payment is due within 14 days. A late fee of 1.5% a month applies after that.\",\n  paymentDetails: [\n    { label: \"Bank\", value: \"Banco Atlântico\" },\n    { label: \"IBAN\", value: \"PT50 0002 0123 1234 5678 9015 4\" },\n    { label: \"Reference\", value: \"INV-2041\" },\n  ],\n}\n\nconst DAY = 86_400_000\nconst toT = (iso: string) => Date.parse(`${iso}T00:00:00Z`)\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"bg-card rounded-3xl border p-8\">\n      <Skeleton className=\"h-8 w-48\" />\n      <Skeleton className=\"mt-8 h-24\" />\n      <Skeleton className=\"mt-8 h-56\" />\n    </div>\n  )\n}\n\n/**\n * An invoice you can read, print and pay: who it is from and to, the dates, each line with its quantity and amount, then the subtotal,\n * discount, tax and total, all worked out from the lines. Its status (paid, due in N days, overdue by N days) comes from the dates and the day\n * you pass, never the clock. Pay now handles the wait and a failure, and the print view hides the buttons.\n */\nfunction Invoice1({ invoice = sample, today = \"2026-10-18\", currency = \"USD\", locale = \"en\", onPay, onDownload, state = \"ready\", onRetry, errorState, className, ...props }: Invoice1Props) {\n  const id = React.useId()\n  const [paidNow, setPaidNow] = React.useState(false)\n  const [paying, setPaying] = React.useState(false)\n  const [payError, setPayError] = React.useState<string>()\n  const money = (n: number) => new Intl.NumberFormat(locale, { style: \"currency\", currency }).format(n)\n  const date = React.useMemo(() => new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"long\", year: \"numeric\", timeZone: \"UTC\" }), [locale])\n\n  const subtotal = invoice.lines.reduce((n, l) => n + l.quantity * l.unitPrice, 0)\n  const discount = (subtotal * (invoice.discountPercent ?? 0)) / 100\n  const taxable = subtotal - discount\n  const tax = (taxable * (invoice.taxPercent ?? 0)) / 100\n  const total = taxable + tax\n\n  const paid = !!invoice.paidOn || paidNow\n  const daysLate = Math.floor((toT(today) - toT(invoice.due)) / DAY)\n  const status = paid\n    ? { tone: \"paid\" as const, label: \"Paid\", note: invoice.paidOn ? `on ${date.format(toT(invoice.paidOn))}` : \"just now\", icon: CircleCheck }\n    : daysLate > 0\n      ? { tone: \"late\" as const, label: \"Overdue\", note: `by ${daysLate} ${daysLate === 1 ? \"day\" : \"days\"}`, icon: CircleAlert }\n      : { tone: \"due\" as const, label: \"Due\", note: daysLate === 0 ? \"today\" : `in ${-daysLate} ${-daysLate === 1 ? \"day\" : \"days\"}`, icon: Clock }\n  const tone = { paid: \"bg-chart-2/10 border-chart-2/30\", late: \"bg-destructive/10 border-destructive/30\", due: \"bg-amber-500/10 border-amber-500/40\" }[status.tone]\n\n  async function pay() {\n    setPayError(undefined)\n    setPaying(true)\n    try {\n      await onPay?.()\n      setPaidNow(true)\n    } catch (e) {\n      setPayError(e instanceof Error ? e.message : \"The payment did not go through. You have not been charged.\")\n    } finally {\n      setPaying(false)\n    }\n  }\n\n  const content = (\n    <article aria-labelledby={`${id}-title`} className=\"bg-card overflow-hidden rounded-3xl border shadow-sm print:border-0 print:shadow-none\">\n      <div className={cn(\"flex flex-wrap items-center justify-between gap-4 border-b px-6 py-4 sm:px-10\", tone)}>\n        <p role=\"status\" className=\"flex items-center gap-2.5 text-sm font-semibold\">\n          <status.icon aria-hidden=\"true\" className=\"size-5\" />\n          {status.label} <span className=\"font-normal\">{status.note}</span>\n        </p>\n        <div className=\"flex flex-wrap gap-2 print:hidden\">\n          {onDownload ? (\n            <Button variant=\"outline\" size=\"sm\" onClick={onDownload}>\n              <Download aria-hidden=\"true\" />\n              Download\n            </Button>\n          ) : null}\n          <Button variant=\"outline\" size=\"sm\" onClick={() => window.print()}>\n            <Printer aria-hidden=\"true\" />\n            Print\n          </Button>\n        </div>\n      </div>\n\n      <div className=\"p-6 sm:p-10\">\n        <div className=\"flex flex-wrap items-start justify-between gap-6\">\n          <div>\n            <span aria-hidden=\"true\" className=\"bg-foreground text-background flex size-11 items-center justify-center rounded-xl text-lg font-semibold\">\n              {invoice.from.name[0]}\n            </span>\n            <h2 id={`${id}-title`} className=\"mt-5 text-3xl font-semibold tracking-[-0.04em]\">\n              Invoice <span className=\"tabular-nums\">{invoice.number}</span>\n            </h2>\n          </div>\n          <div className=\"text-end\">\n            <p className=\"text-muted-foreground text-sm\">Amount {paid ? \"paid\" : \"due\"}</p>\n            <p className=\"text-4xl font-semibold tracking-[-0.045em] tabular-nums sm:text-5xl\">{money(total)}</p>\n          </div>\n        </div>\n\n        <div className=\"mt-10 grid gap-8 sm:grid-cols-3\">\n          <dl>\n            <div>\n              <dt className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">From</dt>\n              <dd className=\"mt-2 text-sm leading-relaxed\">\n                <span className=\"block font-semibold\">{invoice.from.name}</span>\n                {invoice.from.lines.map((l) => (\n                  <span key={l} className=\"text-muted-foreground block\">\n                    {l}\n                  </span>\n                ))}\n              </dd>\n            </div>\n          </dl>\n          <dl>\n            <div>\n              <dt className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">Billed to</dt>\n              <dd className=\"mt-2 text-sm leading-relaxed\">\n                <span className=\"block font-semibold\">{invoice.to.name}</span>\n                {invoice.to.lines.map((l) => (\n                  <span key={l} className=\"text-muted-foreground block\">\n                    {l}\n                  </span>\n                ))}\n              </dd>\n            </div>\n          </dl>\n          <dl className=\"grid content-start gap-3 text-sm\">\n            <div>\n              <dt className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">Issued</dt>\n              <dd className=\"mt-1 font-medium\">{date.format(toT(invoice.issued))}</dd>\n            </div>\n            <div>\n              <dt className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">{paid ? \"Was due\" : \"Due\"}</dt>\n              <dd className=\"mt-1 font-medium\">{date.format(toT(invoice.due))}</dd>\n            </div>\n          </dl>\n        </div>\n\n        <div role=\"region\" tabIndex={0} aria-label=\"Invoice items\" className=\"focus-visible:ring-ring/50 mt-10 overflow-x-auto outline-none focus-visible:ring-[3px]\">\n          <table className=\"w-full min-w-[34rem] text-sm\">\n            <caption className=\"sr-only\">Items on invoice {invoice.number}</caption>\n            <thead>\n              <tr className=\"text-muted-foreground border-b text-xs font-medium tracking-wide uppercase\">\n                <th scope=\"col\" className=\"py-3 pe-4 text-start font-medium\">Description</th>\n                <th scope=\"col\" className=\"px-4 py-3 text-end font-medium\">Qty</th>\n                <th scope=\"col\" className=\"px-4 py-3 text-end font-medium\">Unit price</th>\n                <th scope=\"col\" className=\"py-3 ps-4 text-end font-medium\">Amount</th>\n              </tr>\n            </thead>\n            <tbody className=\"divide-y\">\n              {invoice.lines.map((l) => (\n                <tr key={l.id}>\n                  <td className=\"py-4 pe-4\">\n                    <p className=\"font-medium\">{l.description}</p>\n                    {l.detail ? <p className=\"text-muted-foreground mt-0.5 text-xs\">{l.detail}</p> : null}\n                  </td>\n                  <td className=\"px-4 py-4 text-end tabular-nums\">{l.quantity}</td>\n                  <td className=\"px-4 py-4 text-end tabular-nums\">{money(l.unitPrice)}</td>\n                  <td className=\"py-4 ps-4 text-end font-medium tabular-nums\">{money(l.quantity * l.unitPrice)}</td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </div>\n\n        <dl className=\"mt-6 ms-auto grid max-w-xs gap-2.5 text-sm\">\n          <div className=\"flex justify-between gap-6\">\n            <dt className=\"text-muted-foreground\">Subtotal</dt>\n            <dd className=\"tabular-nums\">{money(subtotal)}</dd>\n          </div>\n          {discount ? (\n            <div className=\"flex justify-between gap-6\">\n              <dt className=\"text-muted-foreground\">Discount ({invoice.discountPercent}%)</dt>\n              <dd className=\"tabular-nums\">−{money(discount)}</dd>\n            </div>\n          ) : null}\n          <div className=\"flex justify-between gap-6\">\n            <dt className=\"text-muted-foreground\">Tax{invoice.taxPercent ? ` (${invoice.taxPercent}%)` : \"\"}</dt>\n            <dd className=\"tabular-nums\">{money(tax)}</dd>\n          </div>\n          <div className=\"flex justify-between gap-6 border-t pt-3 text-base font-semibold\">\n            <dt>Total</dt>\n            <dd className=\"tabular-nums\">{money(total)}</dd>\n          </div>\n        </dl>\n\n        {!paid && invoice.paymentDetails?.length ? (\n          <div className=\"bg-muted/50 mt-10 rounded-2xl border p-5\">\n            <h3 className=\"text-sm font-semibold\">Pay by bank transfer</h3>\n            <dl className=\"mt-3 grid gap-2 text-sm sm:grid-cols-[6rem_minmax(0,1fr)]\">\n              {invoice.paymentDetails.map((d) => (\n                <React.Fragment key={d.label}>\n                  <dt className=\"text-muted-foreground\">{d.label}</dt>\n                  <dd className=\"font-medium tabular-nums\" dir=\"ltr\">\n                    {d.value}\n                  </dd>\n                </React.Fragment>\n              ))}\n            </dl>\n          </div>\n        ) : null}\n\n        {invoice.note ? <p className=\"text-muted-foreground mt-8 max-w-xl text-sm text-pretty\">{invoice.note}</p> : null}\n\n        {!paid && onPay ? (\n          <div className=\"mt-8 border-t pt-6 print:hidden\">\n            <Button size=\"lg\" onClick={pay} disabled={paying} aria-busy={paying}>\n              {paying ? <Loader2 aria-hidden=\"true\" className=\"animate-spin motion-reduce:animate-none\" /> : <CreditCard aria-hidden=\"true\" />}\n              {paying ? \"Paying\" : `Pay ${money(total)} now`}\n            </Button>\n            {payError ? (\n              <p role=\"alert\" className=\"text-destructive mt-3 flex items-start gap-2 text-sm\">\n                <CircleAlert aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0\" />\n                {payError}\n              </p>\n            ) : null}\n          </div>\n        ) : null}\n      </div>\n    </article>\n  )\n\n  return (\n    <section data-slot=\"invoice-1\" data-state={state} aria-label={`Invoice ${invoice.number}`} className={cn(\"mx-auto w-full max-w-3xl px-4 py-10 sm:px-6\", className)} {...frameProps(props)}>\n      <StateView state={state === \"loading\" || state === \"error\" ? state : \"ready\"} onRetry={onRetry} label=\"Loading the invoice\" loading={<Skel />} error={{ title: errorState?.title ?? \"The invoice could not be loaded\", description: errorState?.description ?? \"Check your connection and try again.\", ...errorState }}>\n        {content}\n      </StateView>\n    </section>\n  )\n}\n\nexport { Invoice1, type Invoice1Props, type Invoice1Data, type Invoice1Line }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/invoice-1/invoice-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "invoice",
      "billing",
      "payment",
      "receipt",
      "bill",
      "line items",
      "tax",
      "pay now",
      "print",
      "accounting"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "An invoice. <Invoice1 invoice={{ number, issued, due, paidOn, from: { name, lines }, to: { name, lines }, lines: [{ id, description, detail, quantity, unitPrice }], discountPercent, taxPercent, note, paymentDetails: [{ label, value }] }} today='2026-10-18' currency='USD' locale onPay={() => Promise} onDownload />. Subtotal, discount, tax and total are worked out from the lines; status is paid (paidOn set or payment succeeded), overdue by N days, or due in N days, from due and today. Pass today yourself; the block never reads the clock. Reject onPay to show its message.",
      "whenToUse": [
        "A customer-facing invoice page or a payment link",
        "An invoice detail in a billing area"
      ],
      "whenNotToUse": [
        "A list of invoices (use a data table)",
        "Changing plans with proration (use billing-2)"
      ],
      "composesWith": [
        "billing-1",
        "billing-2",
        "app-shell-1",
        "settings-5"
      ],
      "a11y": [
        {
          "keys": "Status",
          "action": "A live region that says Paid on a date, Due in N days, or Overdue by N days, with an icon, not only color"
        },
        {
          "keys": "Table",
          "action": "The items are a real table with a caption and column headers; totals are a description list"
        },
        {
          "keys": "Pay now",
          "action": "The button says the amount and shows Paying while busy; a failure is an alert that says no charge was made"
        }
      ],
      "customization": [
        "currency and locale format every amount and date",
        "Print hides the buttons and the card border",
        "Bank details are shown left to right in right-to-left layouts, like any number"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "invoice-1-loading",
        "trigger": "state=\"loading\"",
        "note": "A skeleton sheet announced as loading."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "invoice-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert with Try again (onRetry). A failed payment says you were not charged, in place beside Pay now."
      }
    ],
    "examples": [
      "invoice-1-demo",
      "invoice-1-due",
      "invoice-1-paid",
      "invoice-1-loading",
      "invoice-1-error"
    ],
    "url": "https://ui.ballmac.com/components/invoice-1"
  },
  "categories": [
    "blocks",
    "invoice"
  ],
  "type": "registry:block"
}