{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "billing-payment-schedule",
  "title": "Billing Payment Schedule",
  "description": "View scheduled payments with next payment preview and retry options.",
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "progress",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/billing/billing-payment-schedule.tsx",
      "content": "\"use client\";\n\nimport {\n  Calendar,\n  Check,\n  Clock,\n  CreditCard,\n  Loader2,\n  TrendingUp,\n  X,\n} from \"lucide-react\";\nimport { 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 { Progress } from \"@/registry/new-york/ui/progress\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\n\nexport interface ScheduledPayment {\n  id: string;\n  date: Date;\n  amount: number;\n  currency?: string;\n  status: \"upcoming\" | \"processing\" | \"completed\" | \"failed\" | \"canceled\";\n  description: string;\n  paymentMethod?: {\n    type: string;\n    last4?: string;\n    brand?: string;\n  };\n  invoiceId?: string;\n  invoiceNumber?: string;\n  retryAttempts?: number;\n  maxRetryAttempts?: number;\n}\n\nexport interface BillingPaymentScheduleProps {\n  payments: ScheduledPayment[];\n  onViewInvoice?: (invoiceId: string) => void;\n  onRetry?: (paymentId: string) => Promise<void>;\n  onCancel?: (paymentId: string) => Promise<void>;\n  className?: string;\n  currency?: string;\n  showUpcomingOnly?: boolean;\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: ScheduledPayment[\"status\"]) {\n  switch (status) {\n    case \"upcoming\":\n      return {\n        label: \"Upcoming\",\n        variant: \"secondary\" as const,\n        className: \"bg-blue-500/10 text-blue-600 border-blue-500/20\",\n        icon: Clock,\n      };\n    case \"processing\":\n      return {\n        label: \"Processing\",\n        variant: \"secondary\" as const,\n        className: \"bg-yellow-500/10 text-yellow-600 border-yellow-500/20\",\n        icon: Loader2,\n      };\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        icon: Check,\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        icon: TrendingUp,\n      };\n    case \"canceled\":\n      return {\n        label: \"Canceled\",\n        variant: \"secondary\" as const,\n        className: \"\",\n        icon: X,\n      };\n    default:\n      return {\n        label: \"Unknown\",\n        variant: \"secondary\" as const,\n        className: \"\",\n        icon: Clock,\n      };\n  }\n}\n\nfunction getDaysUntil(date: Date): number {\n  const today = new Date();\n  today.setHours(0, 0, 0, 0);\n  const targetDate = new Date(date);\n  targetDate.setHours(0, 0, 0, 0);\n  const diffTime = targetDate.getTime() - today.getTime();\n  const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));\n  return diffDays;\n}\n\nexport default function BillingPaymentSchedule({\n  payments,\n  onViewInvoice,\n  onRetry,\n  onCancel,\n  className,\n  currency = \"USD\",\n  showUpcomingOnly = false,\n}: BillingPaymentScheduleProps) {\n  const [isRetrying, setIsRetrying] = useState<string | null>(null);\n\n  const filteredPayments = showUpcomingOnly\n    ? payments.filter((p) => p.status === \"upcoming\")\n    : payments;\n\n  const sortedPayments = [...filteredPayments].sort(\n    (a, b) => a.date.getTime() - b.date.getTime()\n  );\n\n  const upcomingPayments = payments.filter((p) => p.status === \"upcoming\");\n  const nextPayment =\n    upcomingPayments.length > 0\n      ? upcomingPayments.sort((a, b) => a.date.getTime() - b.date.getTime())[0]\n      : null;\n\n  const handleRetry = async (paymentId: string) => {\n    if (!onRetry) return;\n\n    setIsRetrying(paymentId);\n    try {\n      await onRetry(paymentId);\n    } finally {\n      setIsRetrying(null);\n    }\n  };\n\n  if (payments.length === 0) {\n    return (\n      <Card className={cn(\"w-full shadow-xs\", className)}>\n        <CardHeader>\n          <div className=\"flex flex-col gap-1\">\n            <CardTitle>Payment Schedule</CardTitle>\n            <CardDescription>\n              View your upcoming and past scheduled payments\n            </CardDescription>\n          </div>\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              <Calendar className=\"size-6 text-muted-foreground\" />\n            </div>\n            <p className=\"text-muted-foreground text-sm\">\n              No scheduled payments\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>Payment Schedule</CardTitle>\n            <CardDescription>\n              View your upcoming and past scheduled payments\n            </CardDescription>\n          </div>\n          {nextPayment && (\n            <div className=\"flex flex-col gap-1 text-right\">\n              <div className=\"flex items-center justify-end gap-2 text-muted-foreground text-sm\">\n                <Calendar className=\"size-4\" />\n                <span>Next payment</span>\n              </div>\n              <div className=\"font-medium text-sm\">\n                {formatDate(nextPayment.date)}\n              </div>\n            </div>\n          )}\n        </div>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {nextPayment && (\n            <div className=\"flex flex-col gap-4 rounded-lg border bg-muted/50 p-4\">\n              <div className=\"flex items-center gap-2\">\n                <Clock className=\"size-5 text-blue-600\" />\n                <h3 className=\"font-medium text-sm\">Next Payment</h3>\n              </div>\n              <div className=\"flex flex-col gap-3\">\n                <div className=\"flex items-center justify-between\">\n                  <span className=\"text-muted-foreground text-sm\">Amount</span>\n                  <span className=\"font-medium text-sm\">\n                    {formatPrice(\n                      nextPayment.amount,\n                      nextPayment.currency || currency\n                    )}\n                  </span>\n                </div>\n                <div className=\"flex items-center justify-between\">\n                  <span className=\"text-muted-foreground text-sm\">Date</span>\n                  <span className=\"text-sm\">\n                    {formatDate(nextPayment.date)}\n                  </span>\n                </div>\n                {nextPayment.paymentMethod && (\n                  <div className=\"flex items-center justify-between\">\n                    <span className=\"text-muted-foreground text-sm\">\n                      Payment Method\n                    </span>\n                    <div className=\"flex flex-wrap items-center gap-2 text-sm\">\n                      <span className=\"capitalize\">\n                        {nextPayment.paymentMethod.type}\n                      </span>\n                      {nextPayment.paymentMethod.brand && (\n                        <>\n                          <span aria-hidden=\"true\">•</span>\n                          <span className=\"capitalize\">\n                            {nextPayment.paymentMethod.brand}\n                          </span>\n                        </>\n                      )}\n                      {nextPayment.paymentMethod.last4 && (\n                        <>\n                          <span aria-hidden=\"true\">•</span>\n                          <span>•••• {nextPayment.paymentMethod.last4}</span>\n                        </>\n                      )}\n                    </div>\n                  </div>\n                )}\n                {nextPayment.description && (\n                  <div className=\"flex items-center justify-between\">\n                    <span className=\"text-muted-foreground text-sm\">\n                      Description\n                    </span>\n                    <span className=\"wrap-break-word text-right text-sm\">\n                      {nextPayment.description}\n                    </span>\n                  </div>\n                )}\n                <div className=\"flex items-center gap-2 pt-2\">\n                  <div className=\"flex-1\">\n                    <Progress\n                      aria-label={`Days until payment: ${getDaysUntil(nextPayment.date)}`}\n                      className=\"h-2\"\n                      value={Math.max(\n                        0,\n                        Math.min(\n                          100,\n                          ((30 - getDaysUntil(nextPayment.date)) / 30) * 100\n                        )\n                      )}\n                    />\n                  </div>\n                  <span className=\"shrink-0 text-muted-foreground text-xs\">\n                    {getDaysUntil(nextPayment.date)} day\n                    {getDaysUntil(nextPayment.date) !== 1 ? \"s\" : \"\"} until\n                  </span>\n                </div>\n              </div>\n            </div>\n          )}\n\n          {sortedPayments.length > 0 && (\n            <>\n              {nextPayment && <Separator />}\n              <div className=\"flex flex-col gap-4\">\n                <h3 className=\"font-medium text-sm\">\n                  {showUpcomingOnly ? \"Upcoming Payments\" : \"All Payments\"}\n                </h3>\n                <div className=\"flex flex-col gap-2\">\n                  {sortedPayments.map((payment) => {\n                    const statusConfig = getStatusConfig(payment.status);\n                    const StatusIcon = statusConfig.icon;\n                    const isUpcoming = payment.status === \"upcoming\";\n                    const daysUntil = getDaysUntil(payment.date);\n\n                    return (\n                      <div\n                        className=\"flex flex-col gap-3 rounded-lg border bg-card p-4\"\n                        key={payment.id}\n                      >\n                        <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n                          <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n                            <div className=\"flex flex-wrap items-center gap-2\">\n                              <div className=\"flex items-center gap-2\">\n                                <StatusIcon\n                                  className={cn(\n                                    \"size-4 shrink-0\",\n                                    payment.status === \"processing\" &&\n                                      \"animate-spin\",\n                                    isUpcoming && \"text-blue-600\",\n                                    payment.status === \"completed\" &&\n                                      \"text-green-600\",\n                                    payment.status === \"failed\" &&\n                                      \"text-destructive\"\n                                  )}\n                                />\n                                <span className=\"wrap-break-word font-medium text-sm\">\n                                  {payment.description}\n                                </span>\n                              </div>\n                              <Badge\n                                className={cn(\n                                  \"shrink-0 text-xs\",\n                                  statusConfig.className\n                                )}\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                              <div className=\"flex items-center gap-1\">\n                                <Calendar className=\"size-3.5\" />\n                                <span>{formatDate(payment.date)}</span>\n                              </div>\n                              {isUpcoming && daysUntil >= 0 && (\n                                <>\n                                  <span aria-hidden=\"true\">•</span>\n                                  <span>\n                                    {daysUntil === 0\n                                      ? \"Today\"\n                                      : daysUntil === 1\n                                        ? \"Tomorrow\"\n                                        : `${daysUntil} days`}\n                                  </span>\n                                </>\n                              )}\n                              {payment.invoiceNumber && (\n                                <>\n                                  <span aria-hidden=\"true\">•</span>\n                                  <span>{payment.invoiceNumber}</span>\n                                </>\n                              )}\n                              {payment.paymentMethod && (\n                                <>\n                                  <span aria-hidden=\"true\">•</span>\n                                  <div className=\"flex items-center gap-1\">\n                                    <CreditCard className=\"size-3.5\" />\n                                    <span className=\"capitalize\">\n                                      {payment.paymentMethod.type}\n                                    </span>\n                                    {payment.paymentMethod.last4 && (\n                                      <span>\n                                        {\" \"}\n                                        •••• {payment.paymentMethod.last4}\n                                      </span>\n                                    )}\n                                  </div>\n                                </>\n                              )}\n                              {payment.status === \"failed\" &&\n                                payment.retryAttempts !== undefined &&\n                                payment.maxRetryAttempts !== undefined && (\n                                  <>\n                                    <span aria-hidden=\"true\">•</span>\n                                    <span>\n                                      Retry {payment.retryAttempts}/\n                                      {payment.maxRetryAttempts}\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                                payment.amount,\n                                payment.currency || currency\n                              )}\n                            </span>\n                            <div className=\"flex gap-2\">\n                              {payment.status === \"failed\" && onRetry && (\n                                <Button\n                                  aria-busy={isRetrying === payment.id}\n                                  aria-label={`Retry payment ${payment.id}`}\n                                  data-loading={isRetrying === payment.id}\n                                  onClick={() => handleRetry(payment.id)}\n                                  size=\"icon\"\n                                  type=\"button\"\n                                  variant=\"ghost\"\n                                >\n                                  {isRetrying === payment.id ? (\n                                    <Loader2 className=\"size-4 animate-spin\" />\n                                  ) : (\n                                    <TrendingUp className=\"size-4\" />\n                                  )}\n                                </Button>\n                              )}\n                              {isUpcoming && onCancel && (\n                                <Button\n                                  aria-label={`Cancel payment ${payment.id}`}\n                                  onClick={() => onCancel?.(payment.id)}\n                                  size=\"icon\"\n                                  type=\"button\"\n                                  variant=\"ghost\"\n                                >\n                                  <X className=\"size-4\" />\n                                </Button>\n                              )}\n                              {payment.invoiceId && onViewInvoice && (\n                                <Button\n                                  aria-label={`View invoice ${payment.invoiceNumber}`}\n                                  onClick={() =>\n                                    onViewInvoice(payment.invoiceId!)\n                                  }\n                                  size=\"icon\"\n                                  type=\"button\"\n                                  variant=\"ghost\"\n                                >\n                                  <Calendar className=\"size-4\" />\n                                </Button>\n                              )}\n                            </div>\n                          </div>\n                        </div>\n                      </div>\n                    );\n                  })}\n                </div>\n              </div>\n            </>\n          )}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "billing"
  ],
  "type": "registry:ui"
}