{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "billing-invoice-list",
  "title": "Billing Invoice List",
  "description": "List of invoices with search, filters, and pagination.",
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "input-group",
    "select",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/billing/billing-invoice-list.tsx",
      "content": "\"use client\";\n\nimport { Download, FileText, Search } from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/registry/new-york/ui/badge\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/new-york/ui/card\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/registry/new-york/ui/select\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\n\nexport interface Invoice {\n  id: string;\n  invoiceNumber: string;\n  date: Date;\n  amount: number;\n  currency?: string;\n  status: \"paid\" | \"pending\" | \"failed\" | \"refunded\" | \"void\";\n  description?: string;\n  downloadUrl?: string;\n  items?: InvoiceItem[];\n}\n\nexport interface InvoiceItem {\n  description: string;\n  quantity?: number;\n  amount: number;\n}\n\nexport interface BillingInvoiceListProps {\n  invoices: Invoice[];\n  onDownload?: (invoiceId: string) => void;\n  onViewDetails?: (invoiceId: string) => void;\n  className?: string;\n  showFilters?: boolean;\n  showSearch?: boolean;\n  itemsPerPage?: number;\n  currency?: string;\n}\n\nfunction formatDate(date: Date): string {\n  const year = date.getFullYear();\n  const month = date.toLocaleString(\"en-US\", { month: \"short\" });\n  const day = date.getDate();\n  return `${month} ${day}, ${year}`;\n}\n\nfunction formatPrice(amount: number, currency = \"USD\"): string {\n  return new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\",\n    currency,\n    minimumFractionDigits: 2,\n    maximumFractionDigits: 2,\n  }).format(amount);\n}\n\nfunction getStatusConfig(status: Invoice[\"status\"]) {\n  switch (status) {\n    case \"paid\":\n      return {\n        label: \"Paid\",\n        variant: \"default\" as const,\n        className: \"bg-green-500/10 text-green-600 border-green-500/20\",\n      };\n    case \"pending\":\n      return {\n        label: \"Pending\",\n        variant: \"secondary\" as const,\n        className: \"bg-yellow-500/10 text-yellow-600 border-yellow-500/20\",\n      };\n    case \"failed\":\n      return {\n        label: \"Failed\",\n        variant: \"destructive\" as const,\n        className: \"bg-destructive/10 text-destructive border-destructive/20\",\n      };\n    case \"refunded\":\n      return {\n        label: \"Refunded\",\n        variant: \"secondary\" as const,\n        className: \"bg-blue-500/10 text-blue-600 border-blue-500/20\",\n      };\n    case \"void\":\n      return {\n        label: \"Void\",\n        variant: \"secondary\" as const,\n        className: \"\",\n      };\n    default:\n      return {\n        label: \"Unknown\",\n        variant: \"secondary\" as const,\n        className: \"\",\n      };\n  }\n}\n\nexport default function BillingInvoiceList({\n  invoices,\n  onDownload,\n  onViewDetails,\n  className,\n  showFilters = true,\n  showSearch = true,\n  itemsPerPage = 10,\n  currency = \"USD\",\n}: BillingInvoiceListProps) {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [statusFilter, setStatusFilter] = useState<string>(\"all\");\n  const [currentPage, setCurrentPage] = useState(1);\n\n  const filteredInvoices = useMemo(() => {\n    let filtered = invoices;\n\n    if (searchQuery) {\n      const query = searchQuery.toLowerCase();\n      filtered = filtered.filter(\n        (invoice) =>\n          invoice.invoiceNumber.toLowerCase().includes(query) ||\n          invoice.description?.toLowerCase().includes(query)\n      );\n    }\n\n    if (statusFilter !== \"all\") {\n      filtered = filtered.filter((invoice) => invoice.status === statusFilter);\n    }\n\n    return filtered;\n  }, [invoices, searchQuery, statusFilter]);\n\n  const paginatedInvoices = useMemo(() => {\n    const start = (currentPage - 1) * itemsPerPage;\n    const end = start + itemsPerPage;\n    return filteredInvoices.slice(start, end);\n  }, [filteredInvoices, currentPage, itemsPerPage]);\n\n  const totalPages = Math.ceil(filteredInvoices.length / itemsPerPage);\n\n  if (invoices.length === 0) {\n    return (\n      <Card className={cn(\"w-full shadow-xs\", className)}>\n        <CardHeader>\n          <CardTitle>Invoices</CardTitle>\n          <CardDescription>Your invoice history</CardDescription>\n        </CardHeader>\n        <CardContent>\n          <div className=\"flex flex-col items-center justify-center gap-4 py-12 text-center\">\n            <div className=\"flex size-12 items-center justify-center rounded-full bg-muted\">\n              <FileText className=\"size-6 text-muted-foreground\" />\n            </div>\n            <p className=\"text-muted-foreground text-sm\">No invoices found</p>\n          </div>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  return (\n    <Card className={cn(\"w-full shadow-xs\", className)}>\n      <CardHeader>\n        <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n          <div className=\"flex flex-col gap-1\">\n            <CardTitle>Invoices</CardTitle>\n            <CardDescription>\n              View and download your invoice history\n            </CardDescription>\n          </div>\n          <div className=\"text-muted-foreground text-sm\">\n            {filteredInvoices.length} invoice\n            {filteredInvoices.length !== 1 ? \"s\" : \"\"}\n          </div>\n        </div>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {(showSearch || showFilters) && (\n            <div className=\"flex flex-col gap-4 sm:flex-row\">\n              {showSearch && (\n                <div className=\"flex-1\">\n                  <InputGroup>\n                    <InputGroupAddon>\n                      <Search className=\"size-4\" />\n                    </InputGroupAddon>\n                    <InputGroupInput\n                      onChange={(e) => {\n                        setSearchQuery(e.target.value);\n                        setCurrentPage(1);\n                      }}\n                      placeholder=\"Search by invoice number…\"\n                      type=\"search\"\n                      value={searchQuery}\n                    />\n                  </InputGroup>\n                </div>\n              )}\n              {showFilters && (\n                <Select\n                  onValueChange={(value) => {\n                    setStatusFilter(value);\n                    setCurrentPage(1);\n                  }}\n                  value={statusFilter}\n                >\n                  <SelectTrigger className=\"w-full sm:w-[180px]\">\n                    <SelectValue placeholder=\"Filter by status\" />\n                  </SelectTrigger>\n                  <SelectContent>\n                    <SelectItem value=\"all\">All statuses</SelectItem>\n                    <SelectItem value=\"paid\">Paid</SelectItem>\n                    <SelectItem value=\"pending\">Pending</SelectItem>\n                    <SelectItem value=\"failed\">Failed</SelectItem>\n                    <SelectItem value=\"refunded\">Refunded</SelectItem>\n                    <SelectItem value=\"void\">Void</SelectItem>\n                  </SelectContent>\n                </Select>\n              )}\n            </div>\n          )}\n\n          {paginatedInvoices.length === 0 ? (\n            <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n              <p className=\"text-muted-foreground text-sm\">\n                No invoices match your filters\n              </p>\n            </div>\n          ) : (\n            <>\n              <div className=\"flex flex-col gap-2\">\n                {paginatedInvoices.map((invoice) => {\n                  const statusConfig = getStatusConfig(invoice.status);\n                  return (\n                    <div\n                      className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 sm:flex-row sm:items-center sm:justify-between\"\n                      key={invoice.id}\n                    >\n                      <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n                        <div className=\"flex flex-wrap items-center gap-2\">\n                          <span className=\"font-medium text-sm\">\n                            {invoice.invoiceNumber}\n                          </span>\n                          <Badge\n                            className={cn(\"text-xs\", statusConfig.className)}\n                            variant={statusConfig.variant}\n                          >\n                            {statusConfig.label}\n                          </Badge>\n                        </div>\n                        <div className=\"flex flex-wrap items-center gap-2 text-muted-foreground text-xs\">\n                          <span>{formatDate(invoice.date)}</span>\n                          {invoice.description && (\n                            <>\n                              <span aria-hidden=\"true\">•</span>\n                              <span className=\"wrap-break-word\">\n                                {invoice.description}\n                              </span>\n                            </>\n                          )}\n                        </div>\n                      </div>\n                      <div className=\"flex shrink-0 items-center gap-3\">\n                        <span className=\"font-medium text-sm\">\n                          {formatPrice(\n                            invoice.amount,\n                            invoice.currency || currency\n                          )}\n                        </span>\n                        <div className=\"flex gap-2\">\n                          {onViewDetails && (\n                            <Button\n                              onClick={() => onViewDetails(invoice.id)}\n                              type=\"button\"\n                              variant=\"ghost\"\n                            >\n                              View\n                            </Button>\n                          )}\n                          {onDownload && (\n                            <Button\n                              aria-label={`Download invoice ${invoice.invoiceNumber}`}\n                              onClick={() => onDownload(invoice.id)}\n                              size=\"icon\"\n                              type=\"button\"\n                              variant=\"ghost\"\n                            >\n                              <Download className=\"size-4\" />\n                            </Button>\n                          )}\n                        </div>\n                      </div>\n                    </div>\n                  );\n                })}\n              </div>\n\n              {totalPages > 1 && (\n                <>\n                  <Separator />\n                  <div className=\"flex items-center justify-between\">\n                    <p className=\"text-muted-foreground text-sm\">\n                      Page {currentPage} of {totalPages}\n                    </p>\n                    <div className=\"flex gap-2\">\n                      <Button\n                        disabled={currentPage === 1}\n                        onClick={() => setCurrentPage((p) => p - 1)}\n                        type=\"button\"\n                        variant=\"outline\"\n                      >\n                        Previous\n                      </Button>\n                      <Button\n                        disabled={currentPage === totalPages}\n                        onClick={() => setCurrentPage((p) => p + 1)}\n                        type=\"button\"\n                        variant=\"outline\"\n                      >\n                        Next\n                      </Button>\n                    </div>\n                  </div>\n                </>\n              )}\n            </>\n          )}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "billing"
  ],
  "type": "registry:ui"
}