{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "billing-3",
  "description": "Billing page with plan summary, usage bar, payment method card, and invoice history table",
  "dependencies": [
    "@remixicon/react",
    "@tanstack/react-table",
    "@base-ui/react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "dialog",
    "field",
    "input",
    "progress",
    "separator",
    "sonner",
    "table"
  ],
  "files": [
    {
      "path": "registry/blocks/billing/3/billing-block.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  type ColumnDef,\n  type SortingState,\n  flexRender,\n  getCoreRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  useReactTable,\n} from \"@tanstack/react-table\"\nimport {\n  RiArrowDownLine,\n  RiArrowLeftSLine,\n  RiArrowRightLine,\n  RiArrowRightSLine,\n  RiArrowUpLine,\n  RiBankCardLine,\n  RiCalendarLine,\n  RiDownloadLine,\n  RiExpandUpDownLine,\n  RiHardDriveLine,\n  RiPencilLine,\n} from \"@remixicon/react\"\nimport { toast } from \"sonner\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardAction,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\"\nimport { Field, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Progress } from \"@/components/ui/progress\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Toaster } from \"@/components/ui/sonner\"\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\"\n\nconst PLAN = {\n  name: \"Pro\",\n  price: \"$49\",\n  interval: \"/ Month\",\n  renewalDate: \"July 14, 2026\",\n  storage: {\n    used: 162,\n    total: 250,\n  },\n}\n\nconst PAYMENT = {\n  brand: \"Visa\",\n  last4: \"4242\",\n  expiry: \"08 / 28\",\n  holder: \"Jordan Rivera\",\n}\n\ntype InvoiceStatus = \"Paid\" | \"Pending\" | \"Failed\"\n\ntype Invoice = {\n  id: string\n  date: string\n  amount: string\n  status: InvoiceStatus\n}\n\nconst INVOICES: Invoice[] = [\n  {\n    id: \"INV-2026-0047\",\n    date: \"Jun 1, 2026\",\n    amount: \"$49.00\",\n    status: \"Paid\",\n  },\n  {\n    id: \"INV-2026-0031\",\n    date: \"May 1, 2026\",\n    amount: \"$49.00\",\n    status: \"Paid\",\n  },\n  {\n    id: \"INV-2026-0018\",\n    date: \"Apr 1, 2026\",\n    amount: \"$49.00\",\n    status: \"Pending\",\n  },\n  {\n    id: \"INV-2026-0009\",\n    date: \"Mar 1, 2026\",\n    amount: \"$49.00\",\n    status: \"Paid\",\n  },\n  {\n    id: \"INV-2026-0004\",\n    date: \"Feb 1, 2026\",\n    amount: \"$49.00\",\n    status: \"Failed\",\n  },\n  {\n    id: \"INV-2026-0001\",\n    date: \"Jan 1, 2026\",\n    amount: \"$49.00\",\n    status: \"Paid\",\n  },\n  {\n    id: \"INV-2025-0142\",\n    date: \"Dec 1, 2025\",\n    amount: \"$49.00\",\n    status: \"Paid\",\n  },\n  {\n    id: \"INV-2025-0128\",\n    date: \"Nov 1, 2025\",\n    amount: \"$49.00\",\n    status: \"Paid\",\n  },\n]\n\nconst STATUS_VARIANT: Record<\n  InvoiceStatus,\n  \"default\" | \"secondary\" | \"destructive\" | \"outline\"\n> = {\n  Paid: \"secondary\",\n  Pending: \"outline\",\n  Failed: \"destructive\",\n}\n\nfunction SortIcon({ sorted }: { sorted: false | \"asc\" | \"desc\" }) {\n  if (sorted === \"asc\")\n    return <RiArrowUpLine className=\"size-3.5\" aria-hidden=\"true\" />\n  if (sorted === \"desc\")\n    return <RiArrowDownLine className=\"size-3.5\" aria-hidden=\"true\" />\n  return (\n    <RiExpandUpDownLine\n      className=\"size-3.5 text-muted-foreground/60\"\n      aria-hidden=\"true\"\n    />\n  )\n}\n\nfunction SortHeader({\n  label,\n  sorted,\n  onToggle,\n  align = \"left\",\n}: {\n  label: string\n  sorted: false | \"asc\" | \"desc\"\n  onToggle: () => void\n  align?: \"left\" | \"right\"\n}) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onToggle}\n      className={cn(\n        \"-mx-1 inline-flex items-center gap-1 rounded-none px-1 text-xs font-medium tracking-wide text-muted-foreground uppercase transition-colors hover:text-foreground\",\n        align === \"right\" && \"ml-auto\"\n      )}\n    >\n      {label}\n      <SortIcon sorted={sorted} />\n    </button>\n  )\n}\n\nconst columns: ColumnDef<Invoice>[] = [\n  {\n    accessorKey: \"id\",\n    header: ({ column }) => (\n      <SortHeader\n        label=\"Invoice\"\n        sorted={column.getIsSorted()}\n        onToggle={() => column.toggleSorting(column.getIsSorted() === \"asc\")}\n      />\n    ),\n    cell: ({ row }) => (\n      <span className=\"font-medium text-foreground\">{row.original.id}</span>\n    ),\n  },\n  {\n    accessorKey: \"date\",\n    sortingFn: \"datetime\",\n    header: ({ column }) => (\n      <SortHeader\n        label=\"Date\"\n        sorted={column.getIsSorted()}\n        onToggle={() => column.toggleSorting(column.getIsSorted() === \"asc\")}\n      />\n    ),\n    cell: ({ row }) => (\n      <span className=\"text-muted-foreground\">{row.original.date}</span>\n    ),\n  },\n  {\n    accessorKey: \"amount\",\n    header: ({ column }) => (\n      <SortHeader\n        label=\"Amount\"\n        sorted={column.getIsSorted()}\n        onToggle={() => column.toggleSorting(column.getIsSorted() === \"asc\")}\n      />\n    ),\n    cell: ({ row }) => (\n      <span className=\"text-foreground tabular-nums\">\n        {row.original.amount}\n      </span>\n    ),\n  },\n  {\n    accessorKey: \"status\",\n    enableSorting: false,\n    header: () => (\n      <span className=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\">\n        Status\n      </span>\n    ),\n    cell: ({ row }) => (\n      <Badge variant={STATUS_VARIANT[row.original.status]}>\n        {row.original.status}\n      </Badge>\n    ),\n  },\n  {\n    id: \"download\",\n    enableSorting: false,\n    header: () => (\n      <span className=\"block text-right text-xs font-medium tracking-wide text-muted-foreground uppercase\">\n        Download\n      </span>\n    ),\n    cell: ({ row }) => (\n      <div className=\"flex justify-end\">\n        <Button\n          variant=\"ghost\"\n          size=\"icon-sm\"\n          aria-label={`Download ${row.original.id}`}\n          onClick={() =>\n            toast.success(\"Invoice downloading\", {\n              description: `${row.original.id} (${row.original.amount}) is on its way.`,\n            })\n          }\n        >\n          <RiDownloadLine />\n        </Button>\n      </div>\n    ),\n  },\n]\n\nexport default function BillingBlock() {\n  const autoRenew = true\n  const [sorting, setSorting] = React.useState<SortingState>([])\n  const storagePct = Math.round((PLAN.storage.used / PLAN.storage.total) * 100)\n\n  const table = useReactTable({\n    data: INVOICES,\n    columns,\n    state: { sorting },\n    onSortingChange: setSorting,\n    getCoreRowModel: getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getPaginationRowModel: getPaginationRowModel(),\n    initialState: { pagination: { pageSize: 5 } },\n  })\n\n  const pageCount = table.getPageCount()\n\n  return (\n    <section className=\"flex min-h-svh w-full justify-center bg-muted/30 px-6 py-16 text-foreground\">\n      <Toaster />\n      <div className=\"mx-auto w-full max-w-5xl\">\n        <header className=\"mb-8 flex flex-col gap-1\">\n          <h1 className=\"text-2xl font-semibold tracking-tight\">Billing</h1>\n          <p className=\"text-sm text-muted-foreground\">\n            Manage your Acme subscription, payment method, and invoices.\n          </p>\n        </header>\n\n        <div className=\"grid gap-6 md:grid-cols-5\">\n          <Card className=\"md:col-span-3\">\n            <CardHeader>\n              <div className=\"flex items-center gap-2\">\n                <CardTitle>Current Plan</CardTitle>\n                <Badge variant=\"secondary\">{PLAN.name}</Badge>\n              </div>\n              <CardDescription>\n                {autoRenew\n                  ? \"Your subscription renews automatically.\"\n                  : \"Auto-renew is turned off.\"}\n              </CardDescription>\n            </CardHeader>\n\n            <CardContent className=\"flex flex-col gap-5\">\n              <div className=\"flex items-end gap-1\">\n                <span className=\"text-3xl font-semibold tracking-tight\">\n                  {PLAN.price}\n                </span>\n                <span className=\"mb-1 text-xs text-muted-foreground\">\n                  {PLAN.interval}\n                </span>\n              </div>\n\n              <Separator />\n\n              <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n                <RiCalendarLine className=\"size-3.5 shrink-0\" />\n                <span>\n                  Next Renewal:{\" \"}\n                  <span className=\"font-medium text-foreground\">\n                    {PLAN.renewalDate}\n                  </span>\n                </span>\n              </div>\n\n              <div className=\"flex flex-col gap-2\">\n                <div className=\"flex items-center justify-between text-xs\">\n                  <div className=\"flex items-center gap-2 text-muted-foreground\">\n                    <RiHardDriveLine className=\"size-3.5 shrink-0\" />\n                    <span>Storage Used</span>\n                  </div>\n                  <span className=\"font-medium text-foreground\">\n                    {PLAN.storage.used} GB{\" \"}\n                    <span className=\"text-muted-foreground\">\n                      / {PLAN.storage.total} GB\n                    </span>\n                  </span>\n                </div>\n                <Progress\n                  value={storagePct}\n                  aria-label=\"Storage Used\"\n                  className=\"gap-0\"\n                />\n                <p className=\"text-xs text-muted-foreground\">\n                  {PLAN.storage.total - PLAN.storage.used} GB Remaining\n                </p>\n              </div>\n            </CardContent>\n\n            <CardFooter className=\"flex flex-col gap-2 sm:flex-row\">\n              <UpgradePlanDialog />\n              <Button\n                variant=\"outline\"\n                className=\"w-full sm:flex-1\"\n                onClick={() =>\n                  toast(\"Subscription settings\", {\n                    description: \"Opening your subscription management panel.\",\n                  })\n                }\n              >\n                Manage Subscription\n              </Button>\n            </CardFooter>\n          </Card>\n\n          <Card className=\"md:col-span-2\">\n            <CardHeader>\n              <CardTitle>Payment Method</CardTitle>\n              <CardDescription>Used for recurring charges.</CardDescription>\n              <CardAction>\n                <PaymentMethodDialog\n                  trigger={\n                    <Button variant=\"ghost\" size=\"sm\">\n                      <RiPencilLine data-icon=\"inline-start\" />\n                      Edit\n                    </Button>\n                  }\n                  title=\"Edit Payment Method\"\n                  description=\"Update the card used for your recurring Acme charges.\"\n                  submitLabel=\"Save Card\"\n                  successMessage=\"Payment method updated\"\n                />\n              </CardAction>\n            </CardHeader>\n\n            <CardContent className=\"flex flex-col gap-4\">\n              <div className=\"flex items-center gap-3 border border-border bg-muted/40 p-4\">\n                <span className=\"flex size-10 shrink-0 items-center justify-center bg-background text-foreground\">\n                  <RiBankCardLine className=\"size-5\" />\n                </span>\n                <div className=\"flex flex-col\">\n                  <span className=\"text-sm font-medium\">\n                    {PAYMENT.brand} ending in {PAYMENT.last4}\n                  </span>\n                  <span className=\"text-xs text-muted-foreground\">\n                    Expires {PAYMENT.expiry}\n                  </span>\n                </div>\n              </div>\n\n              <div className=\"flex items-center justify-between text-xs\">\n                <span className=\"text-muted-foreground\">Cardholder</span>\n                <span className=\"font-medium\">{PAYMENT.holder}</span>\n              </div>\n              <div className=\"flex items-center justify-between text-xs\">\n                <span className=\"text-muted-foreground\">Billing email</span>\n                <span className=\"font-medium\">billing@acme.com</span>\n              </div>\n            </CardContent>\n\n            <CardFooter className=\"mt-auto\">\n              <PaymentMethodDialog\n                trigger={\n                  <Button variant=\"outline\" className=\"w-full\">\n                    Add Payment Method\n                  </Button>\n                }\n                title=\"Add Payment Method\"\n                description=\"Add a new card to use for future Acme charges.\"\n                submitLabel=\"Add Card\"\n                successMessage=\"Payment method added\"\n              />\n            </CardFooter>\n          </Card>\n\n          <Card className=\"pb-0 md:col-span-5\">\n            <CardHeader>\n              <CardTitle>Invoice History</CardTitle>\n              <CardDescription>\n                Download or review past invoices.\n              </CardDescription>\n              <CardAction>\n                <span className=\"text-sm text-muted-foreground\">\n                  <span className=\"font-medium text-foreground\">\n                    {INVOICES.length}\n                  </span>{\" \"}\n                  Invoices\n                </span>\n              </CardAction>\n            </CardHeader>\n\n            <CardContent className=\"p-0\">\n              <Table>\n                <TableHeader>\n                  {table.getHeaderGroups().map((headerGroup) => (\n                    <TableRow\n                      key={headerGroup.id}\n                      className=\"border-b border-border hover:bg-transparent\"\n                    >\n                      {headerGroup.headers.map((header) => (\n                        <TableHead\n                          key={header.id}\n                          className={cn(\n                            \"h-10\",\n                            header.column.id === \"id\" && \"pl-6\",\n                            header.column.id === \"download\" && \"pr-6 text-right\"\n                          )}\n                        >\n                          {header.isPlaceholder\n                            ? null\n                            : flexRender(\n                                header.column.columnDef.header,\n                                header.getContext()\n                              )}\n                        </TableHead>\n                      ))}\n                    </TableRow>\n                  ))}\n                </TableHeader>\n                <TableBody>\n                  {table.getRowModel().rows.length ? (\n                    table.getRowModel().rows.map((row) => (\n                      <TableRow\n                        key={row.id}\n                        className=\"border-b border-border last:border-b-0\"\n                      >\n                        {row.getVisibleCells().map((cell) => (\n                          <TableCell\n                            key={cell.id}\n                            className={cn(\n                              \"py-3\",\n                              cell.column.id === \"id\" && \"pl-6\",\n                              cell.column.id === \"download\" && \"pr-6 text-right\"\n                            )}\n                          >\n                            {flexRender(\n                              cell.column.columnDef.cell,\n                              cell.getContext()\n                            )}\n                          </TableCell>\n                        ))}\n                      </TableRow>\n                    ))\n                  ) : (\n                    <TableRow className=\"hover:bg-transparent\">\n                      <TableCell\n                        colSpan={columns.length}\n                        className=\"h-24 text-center text-sm text-muted-foreground\"\n                      >\n                        No invoices yet.\n                      </TableCell>\n                    </TableRow>\n                  )}\n                </TableBody>\n              </Table>\n\n              <div className=\"flex items-center justify-between gap-4 border-t border-border px-6 py-2.5\">\n                <p className=\"text-xs text-muted-foreground tabular-nums\">\n                  Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n                  {Math.max(pageCount, 1)}\n                </p>\n                <div className=\"flex items-center gap-1.5\">\n                  <Button\n                    variant=\"outline\"\n                    size=\"icon\"\n                    className=\"size-7\"\n                    onClick={() => table.previousPage()}\n                    disabled={!table.getCanPreviousPage()}\n                    aria-label=\"Previous page\"\n                  >\n                    <RiArrowLeftSLine className=\"size-3.5\" aria-hidden=\"true\" />\n                  </Button>\n                  <Button\n                    variant=\"outline\"\n                    size=\"icon\"\n                    className=\"size-7\"\n                    onClick={() => table.nextPage()}\n                    disabled={!table.getCanNextPage()}\n                    aria-label=\"Next page\"\n                  >\n                    <RiArrowRightSLine\n                      className=\"size-3.5\"\n                      aria-hidden=\"true\"\n                    />\n                  </Button>\n                </div>\n              </div>\n            </CardContent>\n          </Card>\n        </div>\n      </div>\n    </section>\n  )\n}\n\nconst UPGRADE_PLANS = [\n  { name: \"Team\", price: \"$99\", detail: \"Everything in Pro + admin controls\" },\n  {\n    name: \"Enterprise\",\n    price: \"Custom\",\n    detail: \"SSO, audit logs, and a dedicated success manager\",\n  },\n]\n\nfunction UpgradePlanDialog() {\n  const [open, setOpen] = React.useState(false)\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger\n        render={\n          <Button className=\"w-full sm:flex-1\">\n            Upgrade Plan\n            <RiArrowRightLine data-icon=\"inline-end\" />\n          </Button>\n        }\n      />\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Upgrade Your Plan</DialogTitle>\n          <DialogDescription>\n            You&apos;re on Pro. Pick a plan to unlock more for your team.\n          </DialogDescription>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-2\">\n          {UPGRADE_PLANS.map((plan) => (\n            <button\n              key={plan.name}\n              type=\"button\"\n              onClick={() => {\n                setOpen(false)\n                toast.success(`Switched to ${plan.name}`, {\n                  description: `Your plan will update on your next renewal.`,\n                })\n              }}\n              className=\"flex items-center justify-between gap-4 border border-border p-3 text-left transition-colors hover:bg-muted\"\n            >\n              <span className=\"flex flex-col gap-0.5\">\n                <span className=\"text-sm font-medium\">{plan.name}</span>\n                <span className=\"max-w-[15rem] text-xs text-muted-foreground\">\n                  {plan.detail}\n                </span>\n              </span>\n              <span className=\"shrink-0 text-sm font-semibold tabular-nums\">\n                {plan.price}\n              </span>\n            </button>\n          ))}\n        </div>\n        <DialogFooter showCloseButton />\n      </DialogContent>\n    </Dialog>\n  )\n}\n\nfunction PaymentMethodDialog({\n  trigger,\n  title,\n  description,\n  submitLabel,\n  successMessage,\n}: {\n  trigger: React.ReactElement\n  title: string\n  description: string\n  submitLabel: string\n  successMessage: string\n}) {\n  const [open, setOpen] = React.useState(false)\n\n  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault()\n    setOpen(false)\n    toast.success(successMessage, {\n      description: \"Your billing details are now up to date.\",\n    })\n  }\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger render={trigger} />\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{title}</DialogTitle>\n          <DialogDescription>{description}</DialogDescription>\n        </DialogHeader>\n        <form id=\"payment-form\" onSubmit={handleSubmit} className=\"grid gap-4\">\n          <Field>\n            <FieldLabel htmlFor=\"card-name\">Cardholder name</FieldLabel>\n            <Input id=\"card-name\" placeholder=\"Jordan Rivera\" />\n          </Field>\n          <Field>\n            <FieldLabel htmlFor=\"card-number\">Card number</FieldLabel>\n            <Input id=\"card-number\" placeholder=\"1234 5678 9012 3456\" />\n          </Field>\n          <div className=\"grid grid-cols-2 gap-4\">\n            <Field>\n              <FieldLabel htmlFor=\"card-expiry\">Expiry</FieldLabel>\n              <Input id=\"card-expiry\" placeholder=\"MM / YY\" />\n            </Field>\n            <Field>\n              <FieldLabel htmlFor=\"card-cvc\">CVC</FieldLabel>\n              <Input id=\"card-cvc\" placeholder=\"123\" />\n            </Field>\n          </div>\n        </form>\n        <DialogFooter>\n          <DialogClose render={<Button variant=\"outline\" />}>\n            Cancel\n          </DialogClose>\n          <Button type=\"submit\" form=\"payment-form\">\n            {submitLabel}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "height": "90vh",
    "tier": "free"
  },
  "categories": [
    "billing"
  ],
  "type": "registry:block"
}