{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "billing-billing-history",
  "title": "Billing Billing History",
  "description": "Display transaction history with search, filters, and pagination.",
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "input-group",
    "select",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/billing/billing-billing-history.tsx",
      "content": "\"use client\";\n\nimport { ArrowDown, ArrowUp, 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 BillingTransaction {\n  id: string;\n  date: Date;\n  type: \"charge\" | \"refund\" | \"credit\" | \"adjustment\" | \"subscription\";\n  amount: number;\n  currency?: string;\n  description: string;\n  status: \"completed\" | \"pending\" | \"failed\";\n  invoiceId?: string;\n  paymentMethod?: string;\n  metadata?: Record<string, unknown>;\n}\n\nexport interface BillingBillingHistoryProps {\n  transactions: BillingTransaction[];\n  onViewDetails?: (transactionId: string) => void;\n  onExport?: () => void;\n  className?: string;\n  showFilters?: boolean;\n  showSearch?: boolean;\n  showSummary?: boolean;\n  currency?: string;\n  itemsPerPage?: number;\n  groupBy?: \"none\" | \"month\" | \"year\";\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  const sign = amount >= 0 ? \"\" : \"-\";\n  return `${sign}${new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\",\n    currency,\n    minimumFractionDigits: 2,\n    maximumFractionDigits: 2,\n  }).format(Math.abs(amount))}`;\n}\n\nfunction getTypeConfig(type: BillingTransaction[\"type\"]) {\n  switch (type) {\n    case \"charge\":\n      return { label: \"Charge\", icon: ArrowDown, className: \"text-green-600\" };\n    case \"refund\":\n      return { label: \"Refund\", icon: ArrowUp, className: \"text-blue-600\" };\n    case \"credit\":\n      return { label: \"Credit\", icon: ArrowUp, className: \"text-blue-600\" };\n    case \"adjustment\":\n      return {\n        label: \"Adjustment\",\n        icon: ArrowUp,\n        className: \"text-yellow-600\",\n      };\n    case \"subscription\":\n      return {\n        label: \"Subscription\",\n        icon: ArrowDown,\n        className: \"text-primary\",\n      };\n    default:\n      return { label: \"Transaction\", icon: FileText, className: \"\" };\n  }\n}\n\nfunction getStatusConfig(status: BillingTransaction[\"status\"]) {\n  switch (status) {\n    case \"completed\":\n      return {\n        label: \"Completed\",\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    default:\n      return {\n        label: \"Unknown\",\n        variant: \"secondary\" as const,\n        className: \"\",\n      };\n  }\n}\n\nexport default function BillingBillingHistory({\n  transactions,\n  onViewDetails,\n  onExport,\n  className,\n  showFilters = true,\n  showSearch = true,\n  showSummary = true,\n  currency = \"USD\",\n  itemsPerPage = 10,\n  groupBy = \"none\",\n}: BillingBillingHistoryProps) {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [typeFilter, setTypeFilter] = useState<string>(\"all\");\n  const [statusFilter, setStatusFilter] = useState<string>(\"all\");\n  const [sortBy, setSortBy] = useState<\"date\" | \"amount\">(\"date\");\n  const [sortOrder, setSortOrder] = useState<\"asc\" | \"desc\">(\"desc\");\n  const [currentPage, setCurrentPage] = useState(1);\n\n  const filteredTransactions = useMemo(() => {\n    let filtered = transactions;\n\n    if (searchQuery) {\n      const query = searchQuery.toLowerCase();\n      filtered = filtered.filter(\n        (tx) =>\n          tx.description.toLowerCase().includes(query) ||\n          tx.id.toLowerCase().includes(query) ||\n          tx.invoiceId?.toLowerCase().includes(query) ||\n          formatPrice(tx.amount, tx.currency || currency)\n            .toLowerCase()\n            .includes(query)\n      );\n    }\n\n    if (typeFilter !== \"all\") {\n      filtered = filtered.filter((tx) => tx.type === typeFilter);\n    }\n\n    if (statusFilter !== \"all\") {\n      filtered = filtered.filter((tx) => tx.status === statusFilter);\n    }\n\n    filtered.sort((a, b) => {\n      let comparison = 0;\n      if (sortBy === \"date\") {\n        comparison = a.date.getTime() - b.date.getTime();\n      } else {\n        comparison = a.amount - b.amount;\n      }\n      return sortOrder === \"asc\" ? comparison : -comparison;\n    });\n\n    return filtered;\n  }, [\n    transactions,\n    searchQuery,\n    typeFilter,\n    statusFilter,\n    sortBy,\n    sortOrder,\n    currency,\n  ]);\n\n  const summary = useMemo(() => {\n    const charges = filteredTransactions\n      .filter((tx) => tx.type === \"charge\" && tx.status === \"completed\")\n      .reduce((sum, tx) => sum + tx.amount, 0);\n    const refunds = filteredTransactions\n      .filter((tx) => tx.type === \"refund\" && tx.status === \"completed\")\n      .reduce((sum, tx) => sum + Math.abs(tx.amount), 0);\n    const credits = filteredTransactions\n      .filter((tx) => tx.type === \"credit\" && tx.status === \"completed\")\n      .reduce((sum, tx) => sum + Math.abs(tx.amount), 0);\n    return { charges, refunds, credits, net: charges - refunds - credits };\n  }, [filteredTransactions]);\n\n  const paginatedTransactions = useMemo(() => {\n    const start = (currentPage - 1) * itemsPerPage;\n    const end = start + itemsPerPage;\n    return filteredTransactions.slice(start, end);\n  }, [filteredTransactions, currentPage, itemsPerPage]);\n\n  const totalPages = Math.ceil(filteredTransactions.length / itemsPerPage);\n\n  if (transactions.length === 0) {\n    return (\n      <Card className={cn(\"w-full shadow-xs\", className)}>\n        <CardHeader>\n          <CardTitle>Billing history</CardTitle>\n          <CardDescription>Your transaction 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\">\n              No transactions found\n            </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>Billing history</CardTitle>\n            <CardDescription>View all your transactions</CardDescription>\n          </div>\n          {onExport && (\n            <Button\n              className=\"w-full sm:w-auto\"\n              onClick={onExport}\n              type=\"button\"\n              variant=\"outline\"\n            >\n              <Download className=\"size-4\" />\n              Export CSV\n            </Button>\n          )}\n        </div>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {showSummary && (\n            <div className=\"grid grid-cols-2 gap-4 rounded-lg border bg-muted/50 p-4 sm:grid-cols-4\">\n              <div className=\"flex flex-col gap-1\">\n                <span className=\"text-muted-foreground text-xs\">\n                  Total charges\n                </span>\n                <span className=\"font-semibold text-sm\">\n                  {formatPrice(summary.charges, currency)}\n                </span>\n              </div>\n              <div className=\"flex flex-col gap-1\">\n                <span className=\"text-muted-foreground text-xs\">Refunds</span>\n                <span className=\"font-semibold text-sm\">\n                  {formatPrice(summary.refunds, currency)}\n                </span>\n              </div>\n              <div className=\"flex flex-col gap-1\">\n                <span className=\"text-muted-foreground text-xs\">Credits</span>\n                <span className=\"font-semibold text-sm\">\n                  {formatPrice(summary.credits, currency)}\n                </span>\n              </div>\n              <div className=\"flex flex-col gap-1\">\n                <span className=\"text-muted-foreground text-xs\">Net total</span>\n                <span className=\"font-semibold text-sm\">\n                  {formatPrice(summary.net, currency)}\n                </span>\n              </div>\n            </div>\n          )}\n\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 transactions…\"\n                      type=\"search\"\n                      value={searchQuery}\n                    />\n                  </InputGroup>\n                </div>\n              )}\n              {showFilters && (\n                <div className=\"flex flex-col gap-2 sm:flex-row\">\n                  <Select\n                    onValueChange={(value) => {\n                      setTypeFilter(value);\n                      setCurrentPage(1);\n                    }}\n                    value={typeFilter}\n                  >\n                    <SelectTrigger className=\"w-full sm:w-[140px]\">\n                      <SelectValue placeholder=\"Type\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      <SelectItem value=\"all\">All types</SelectItem>\n                      <SelectItem value=\"charge\">Charges</SelectItem>\n                      <SelectItem value=\"refund\">Refunds</SelectItem>\n                      <SelectItem value=\"credit\">Credits</SelectItem>\n                      <SelectItem value=\"adjustment\">Adjustments</SelectItem>\n                      <SelectItem value=\"subscription\">\n                        Subscriptions\n                      </SelectItem>\n                    </SelectContent>\n                  </Select>\n                  <Select\n                    onValueChange={(value) => {\n                      setStatusFilter(value);\n                      setCurrentPage(1);\n                    }}\n                    value={statusFilter}\n                  >\n                    <SelectTrigger className=\"w-full sm:w-[140px]\">\n                      <SelectValue placeholder=\"Status\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      <SelectItem value=\"all\">All statuses</SelectItem>\n                      <SelectItem value=\"completed\">Completed</SelectItem>\n                      <SelectItem value=\"pending\">Pending</SelectItem>\n                      <SelectItem value=\"failed\">Failed</SelectItem>\n                    </SelectContent>\n                  </Select>\n                  <Select\n                    onValueChange={(value) => {\n                      const [by, order] = value.split(\"-\") as [\n                        \"date\" | \"amount\",\n                        \"asc\" | \"desc\",\n                      ];\n                      setSortBy(by);\n                      setSortOrder(order);\n                      setCurrentPage(1);\n                    }}\n                    value={`${sortBy}-${sortOrder}`}\n                  >\n                    <SelectTrigger className=\"w-full sm:w-[140px]\">\n                      <SelectValue placeholder=\"Sort\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      <SelectItem value=\"date-desc\">Newest first</SelectItem>\n                      <SelectItem value=\"date-asc\">Oldest first</SelectItem>\n                      <SelectItem value=\"amount-desc\">\n                        Amount: High to Low\n                      </SelectItem>\n                      <SelectItem value=\"amount-asc\">\n                        Amount: Low to High\n                      </SelectItem>\n                    </SelectContent>\n                  </Select>\n                </div>\n              )}\n            </div>\n          )}\n\n          {paginatedTransactions.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 transactions match your filters\n              </p>\n            </div>\n          ) : (\n            <>\n              <div className=\"flex flex-col gap-2\">\n                {paginatedTransactions.map((transaction) => {\n                  const typeConfig = getTypeConfig(transaction.type);\n                  const statusConfig = getStatusConfig(transaction.status);\n                  const TypeIcon = typeConfig.icon;\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={transaction.id}\n                    >\n                      <div className=\"flex min-w-0 flex-1 items-center gap-3\">\n                        <div\n                          className={cn(\n                            \"flex size-8 shrink-0 items-center justify-center rounded-lg border\",\n                            typeConfig.className\n                          )}\n                        >\n                          <TypeIcon className=\"size-4\" />\n                        </div>\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=\"wrap-break-word font-medium text-sm\">\n                              {transaction.description}\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(transaction.date)}</span>\n                            {transaction.invoiceId && (\n                              <>\n                                <span aria-hidden=\"true\">•</span>\n                                <span>{transaction.invoiceId}</span>\n                              </>\n                            )}\n                            {transaction.paymentMethod && (\n                              <>\n                                <span aria-hidden=\"true\">•</span>\n                                <span>{transaction.paymentMethod}</span>\n                              </>\n                            )}\n                          </div>\n                        </div>\n                      </div>\n                      <div className=\"flex shrink-0 items-center gap-3\">\n                        <span\n                          className={cn(\n                            \"font-medium text-sm\",\n                            transaction.amount < 0 && \"text-green-600\",\n                            transaction.amount > 0 && \"text-foreground\"\n                          )}\n                        >\n                          {formatPrice(\n                            transaction.amount,\n                            transaction.currency || currency\n                          )}\n                        </span>\n                        {onViewDetails && (\n                          <Button\n                            onClick={() => onViewDetails(transaction.id)}\n                            type=\"button\"\n                            variant=\"ghost\"\n                          >\n                            View\n                          </Button>\n                        )}\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"
}