{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "billing-card",
  "title": "Billing Card",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A subscription summary with plan and price, status, next charge, payment method, seat usage and recent invoices, including failed-payment and trial states.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/billing-card.tsx",
      "content": "// Ballmac UI: Billing Card. https://ui.ballmac.com/components/billing-card\n\"use client\";\n\nimport * as React from \"react\";\nimport { AlertTriangle, CalendarClock, CircleCheck, Clock, Download, XCircle } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { useLocale, useMessages, defineMessage } from \"@/lib/ballmac/i18n\";\n\ntype BillingStatus = \"active\" | \"trialing\" | \"past_due\" | \"canceled\";\n\ntype BillingPlan = {\n  /** Plan name, for example \"Team\". */\n  name: string;\n  /** Price per interval in major currency units (for example 49 for $49). */\n  price: number;\n  /** Billing interval. */\n  interval: \"month\" | \"year\";\n  /** ISO 4217 currency code. */\n  currency?: string;\n  /** Subscription state. */\n  status: BillingStatus;\n  /** ISO date (YYYY-MM-DD) of the next charge, or when access ends if canceled. */\n  renewsOn?: string;\n  /** ISO date (YYYY-MM-DD) the trial ends. */\n  trialEndsOn?: string;\n  /** Seats in use and included. */\n  seats?: { used: number; total: number };\n};\n\ntype BillingPaymentMethod = {\n  /** Card brand as text, for example \"Visa\". */\n  brand: string;\n  /** Last four digits. */\n  last4: string;\n  /** Expiry as MM/YY. */\n  expires: string;\n};\n\ntype BillingInvoice = {\n  id: string;\n  /** ISO date (YYYY-MM-DD). */\n  date: string;\n  /** Amount in major currency units. */\n  amount: number;\n  status: \"paid\" | \"open\" | \"failed\";\n  /** Link to download the PDF. */\n  href?: string;\n};\n\ntype BillingCardProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  plan: BillingPlan;\n  paymentMethod?: BillingPaymentMethod;\n  /** Recent invoices, newest first. The first three are shown. */\n  invoices?: BillingInvoice[];\n  /** Adds a \"Change plan\" button and calls this when pressed. */\n  onChangePlan?: () => void;\n  /** Adds an \"Update\" button on the payment method row. */\n  onUpdatePayment?: () => void;\n  /** Adds a \"Cancel subscription\" text button. */\n  onCancel?: () => void;\n  /** Locale for dates and money. Fixed by default so server and browser match. */\n  locale?: string;\n};\n\nconst STATUS = {\n  active: { label: defineMessage(\"billing-card.STATUS.active\", \"Active\"), icon: CircleCheck, tone: \"bg-chart-2/12 text-foreground\", iconTone: \"text-chart-2\" },\n  trialing: { label: defineMessage(\"billing-card.STATUS.trialing\", \"Trial\"), icon: Clock, tone: \"bg-chart-1/12 text-foreground\", iconTone: \"text-chart-1\" },\n  past_due: { label: defineMessage(\"billing-card.STATUS.past_due\", \"Payment failed\"), icon: AlertTriangle, tone: \"bg-destructive/12 text-foreground\", iconTone: \"text-destructive\" },\n  canceled: { label: defineMessage(\"billing-card.STATUS.canceled\", \"Canceled\"), icon: XCircle, tone: \"bg-muted text-foreground\", iconTone: \"text-muted-foreground\" },\n} as const;\n\nconst INVOICE_STATUS = {\n  paid: { label: defineMessage(\"billing-card.INVOICE_STATUS.paid\", \"Paid\"), dot: \"bg-chart-2\" },\n  open: { label: defineMessage(\"billing-card.INVOICE_STATUS.open\", \"Open\"), dot: \"bg-chart-3\" },\n  failed: { label: defineMessage(\"billing-card.INVOICE_STATUS.failed\", \"Failed\"), dot: \"bg-destructive\" },\n} as const;\n\n/**\n * A subscription summary: plan and price, status, next charge, payment method, seat usage and recent invoices.\n * Status uses an icon and words, not color alone. Dates are formatted in UTC from ISO strings so they never shift.\n */\nfunction BillingCard({\n  plan,\n  paymentMethod,\n  invoices = [],\n  onChangePlan,\n  onUpdatePayment,\n  onCancel,\n  locale,\n  className,\n  ...props\n}: BillingCardProps) {\n  const defaultLocale = useLocale()\n  locale ??= defaultLocale\n  const msg = useMessages()\n  const currency = plan.currency ?? \"USD\";\n  const money = (n: number, digits = 2) =>\n    new Intl.NumberFormat(locale, { style: \"currency\", currency, minimumFractionDigits: digits, maximumFractionDigits: digits }).format(n);\n  const date = (iso?: string) =>\n    iso ? new Intl.DateTimeFormat(locale, { dateStyle: \"medium\", timeZone: \"UTC\" }).format(new Date(`${iso}T00:00:00Z`)) : \"\";\n  const status = STATUS[plan.status];\n  const StatusIcon = status.icon;\n  const whole = Math.trunc(plan.price);\n  const cents = Math.round((plan.price - whole) * 100);\n  const symbol = money(0, 0).replace(/[\\d\\s.,]/g, \"\");\n  const seatPercent = plan.seats ? Math.min(100, (plan.seats.used / Math.max(plan.seats.total, 1)) * 100) : 0;\n  const nextLabel =\n    plan.status === \"canceled\" ? \"Access ends\" : plan.status === \"trialing\" ? \"Trial ends\" : plan.status === \"past_due\" ? \"Retrying\" : \"Renews\";\n  const nextDate = plan.status === \"trialing\" ? plan.trialEndsOn : plan.renewsOn;\n  const headingId = React.useId();\n\n  return (\n    <section\n      data-slot=\"billing-card\"\n      aria-labelledby={headingId}\n      className={cn(\n        \"w-full overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-[0_1px_2px_rgb(0_0_0/0.04),0_16px_40px_-20px_rgb(0_0_0/0.2)]\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"relative overflow-hidden border-b bg-gradient-to-br from-chart-1/[0.10] via-chart-4/[0.06] to-transparent p-6\">\n        <div aria-hidden=\"true\" className=\"pointer-events-none absolute -top-16 -end-10 size-48 rounded-full bg-chart-1/10 blur-3xl\" />\n        <div className=\"relative flex flex-wrap items-start justify-between gap-4\">\n          <div>\n            <p className=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\">{msg(\"billing-card.currentPlan\", \"Current plan\")}</p>\n            <h3 id={headingId} className=\"mt-1 text-xl font-semibold tracking-tight\">\n              {plan.name}\n            </h3>\n          </div>\n          <span className={cn(\"inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium\", status.tone)}>\n            <StatusIcon aria-hidden=\"true\" className={cn(\"size-3.5\", status.iconTone)} />\n            {msg.of(status.label)}\n          </span>\n        </div>\n        <p className=\"relative mt-5 flex items-baseline tabular-nums\">\n          <span className=\"me-0.5 text-2xl font-medium text-muted-foreground\">{symbol}</span>\n          <span className=\"text-5xl font-semibold tracking-tight\">{whole.toLocaleString(locale)}</span>\n          {cents > 0 && <span className=\"text-xl font-medium text-muted-foreground\">.{String(cents).padStart(2, \"0\")}</span>}\n          <span className=\"ms-1.5 text-sm text-muted-foreground\">/ {plan.interval}</span>\n        </p>\n      </div>\n\n      {plan.status === \"past_due\" && (\n        <p role=\"alert\" className=\"flex items-start gap-2.5 border-b bg-destructive/[0.06] px-6 py-3 text-sm\">\n          <AlertTriangle aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-destructive\" />\n          <span>We could not charge your card. Update your payment method to keep your workspace active.</span>\n        </p>\n      )}\n\n      <div className=\"grid gap-px bg-border sm:grid-cols-2 [&>div]:bg-card\">\n        {nextDate && (\n          <div className={cn(\"flex items-start gap-3 p-5\", !plan.seats && \"sm:col-span-2\")}>\n            <CalendarClock aria-hidden=\"true\" className=\"mt-0.5 size-4 text-muted-foreground\" />\n            <div>\n              <p className=\"text-xs text-muted-foreground\">{nextLabel}</p>\n              <p className=\"mt-0.5 text-sm font-medium\">{date(nextDate)}</p>\n            </div>\n          </div>\n        )}\n        {plan.seats && (\n          <div className=\"grid content-center gap-2 p-5\">\n            <div className=\"flex items-baseline justify-between gap-3 text-sm\">\n              <p className=\"text-xs text-muted-foreground\">{msg(\"billing-card.seats\", \"Seats\")}</p>\n              <p className=\"font-medium tabular-nums\">\n                {msg(\"billing-card.seatsOf\", \"{used} of {total} used\", { used: plan.seats.used, total: plan.seats.total })}\n              </p>\n            </div>\n            <div\n              role=\"meter\"\n              aria-label={msg(\"billing-card.seatsUsed\", \"Seats used\")}\n              aria-valuemin={0}\n              aria-valuemax={plan.seats.total}\n              aria-valuenow={plan.seats.used}\n              className=\"h-1.5 overflow-hidden rounded-full bg-muted\"\n            >\n              <div className=\"h-full rounded-full bg-primary transition-[width] duration-700 motion-reduce:transition-none\" style={{ width: `${seatPercent}%` }} />\n            </div>\n          </div>\n        )}\n        {paymentMethod && (\n          <div className=\"flex items-center gap-3 p-5 sm:col-span-2\">\n            <span\n              aria-hidden=\"true\"\n              className=\"relative flex h-8 w-12 shrink-0 items-end justify-end overflow-hidden rounded-md bg-gradient-to-br from-foreground to-foreground/70 p-1 shadow-sm\"\n            >\n              <span className=\"absolute top-1.5 start-1.5 h-2 w-3 rounded-[2px] bg-background/30\" />\n              <span className=\"text-[8px] font-bold tracking-wider text-background/90 uppercase\">{paymentMethod.brand.slice(0, 4)}</span>\n            </span>\n            <div className=\"min-w-0 flex-1\">\n              <p className=\"text-xs text-muted-foreground\">{msg(\"billing-card.paymentMethod\", \"Payment method\")}</p>\n              <p className=\"mt-0.5 truncate text-sm font-medium tabular-nums\">\n                {paymentMethod.brand} •••• {paymentMethod.last4}\n                <span className=\"ms-2 text-xs font-normal text-muted-foreground\">{msg(\"billing-card.exp\", \"Exp {date}\", { date: paymentMethod.expires })}</span>\n              </p>\n            </div>\n            {onUpdatePayment && (\n              <button\n                type=\"button\"\n                onClick={onUpdatePayment}\n                className=\"rounded-md px-2 py-1 text-[13px] font-medium underline-offset-4 outline-none hover:underline focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n              >\n                {msg(\"billing-card.update\", \"Update\")}\n              </button>\n            )}\n          </div>\n        )}\n      </div>\n\n      {invoices.length > 0 && (\n        <div className=\"border-t\">\n          <h4 className=\"px-6 pt-4 pb-2 text-xs font-medium tracking-wide text-muted-foreground uppercase\">{msg(\"billing-card.recentInvoices\", \"Recent invoices\")}</h4>\n          <ul className=\"px-3 pb-3\">\n            {invoices.slice(0, 3).map((inv) => {\n              const s = INVOICE_STATUS[inv.status];\n              return (\n                <li key={inv.id} className=\"flex items-center gap-3 rounded-lg px-3 py-2 text-sm hover:bg-muted/50\">\n                  <span className=\"w-28 shrink-0 text-muted-foreground tabular-nums\">{date(inv.date)}</span>\n                  <span className=\"flex-1 font-medium tabular-nums\">{money(inv.amount)}</span>\n                  <span className=\"inline-flex items-center gap-1.5 text-xs font-medium\">\n                    <span aria-hidden=\"true\" className={cn(\"size-1.5 rounded-full\", s.dot)} />\n                    {msg.of(s.label)}\n                  </span>\n                  {inv.href && (\n                    <a\n                      href={inv.href}\n                      aria-label={msg(\"billing-card.downloadInvoice\", \"Download invoice {id}\", { id: inv.id })}\n                      className=\"inline-flex size-8 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n                    >\n                      <Download aria-hidden=\"true\" className=\"size-4\" />\n                    </a>\n                  )}\n                </li>\n              );\n            })}\n          </ul>\n        </div>\n      )}\n\n      {(onChangePlan || onCancel) && (\n        <div className=\"flex flex-wrap items-center gap-2 border-t bg-muted/30 px-6 py-4\">\n          {onChangePlan && (\n            <button\n              type=\"button\"\n              onClick={onChangePlan}\n              className=\"inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow-xs outline-none transition-colors hover:bg-primary/90 focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n            >\n              {msg(\"billing-card.changePlan\", \"Change plan\")}\n            </button>\n          )}\n          {onCancel && plan.status !== \"canceled\" && (\n            <button\n              type=\"button\"\n              onClick={onCancel}\n              className=\"ms-auto rounded-md px-2 py-1 text-[13px] text-muted-foreground underline-offset-4 outline-none hover:text-destructive hover:underline focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n            >\n              {msg(\"billing-card.cancelSubscription\", \"Cancel subscription\")}\n            </button>\n          )}\n        </div>\n      )}\n    </section>\n  );\n}\n\nexport { BillingCard, type BillingCardProps, type BillingPlan, type BillingPaymentMethod, type BillingInvoice, type BillingStatus };\n",
      "type": "registry:ui",
      "target": "@components/ballmac/billing-card.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "billing",
      "subscription",
      "invoices",
      "plan"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "plan {name,price,interval,status,renewsOn,seats}, paymentMethod, invoices[], and onChangePlan / onUpdatePayment / onCancel. Dates are ISO strings formatted in UTC.",
      "whenToUse": [
        "Account billing pages",
        "Showing subscription state in settings"
      ],
      "whenNotToUse": [
        "Choosing a plan; use plan-selector",
        "A line-item invoice table; use table"
      ],
      "composesWith": [
        "usage-meter",
        "plan-selector",
        "settings-panel"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "Details are a description list; a failed payment is announced as an alert"
        },
        {
          "keys": "Tab",
          "action": "Change plan, Update, Cancel and invoice downloads are real buttons and links"
        }
      ],
      "customization": [
        "currency and locale",
        "status: active | trialing | past_due | canceled",
        "seats",
        "invoices"
      ]
    },
    "examples": [
      "billing-card-demo",
      "billing-card-states"
    ],
    "url": "https://ui.ballmac.com/components/billing-card"
  },
  "categories": [
    "saas"
  ],
  "type": "registry:ui"
}