{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "billing-payment-form",
  "title": "Billing Payment Form",
  "description": "Form to add payment methods with card details and billing address.",
  "registryDependencies": [
    "button",
    "card",
    "checkbox",
    "field",
    "input-group",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/billing/billing-payment-form.tsx",
      "content": "\"use client\";\n\nimport { CreditCard, Loader2, Lock, Shield } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\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 { Checkbox } from \"@/registry/new-york/ui/checkbox\";\nimport {\n  Field,\n  FieldContent,\n  FieldError,\n  FieldLabel,\n} from \"@/registry/new-york/ui/field\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\n\nexport interface PaymentFormData {\n  cardNumber: string;\n  expiryMonth: string;\n  expiryYear: string;\n  cvv: string;\n  holderName: string;\n  billingAddress?: {\n    line1: string;\n    line2?: string;\n    city: string;\n    state: string;\n    zip: string;\n    country: string;\n  };\n  saveForFuture?: boolean;\n  setAsDefault?: boolean;\n}\n\nexport interface BillingPaymentFormProps {\n  onSubmit?: (data: PaymentFormData) => void;\n  onCancel?: () => void;\n  defaultValues?: Partial<PaymentFormData>;\n  className?: string;\n  isLoading?: boolean;\n  errors?: {\n    cardNumber?: string;\n    expiry?: string;\n    cvv?: string;\n    holderName?: string;\n    billingAddress?: Record<string, string>;\n    general?: string;\n  };\n  showBillingAddress?: boolean;\n  showSaveOption?: boolean;\n  showSetDefault?: boolean;\n  currency?: string;\n}\n\nfunction formatCardNumber(value: string): string {\n  const cleaned = value.replace(/\\s+/g, \"\");\n  const chunks = cleaned.match(/.{1,4}/g) || [];\n  return chunks.join(\" \").slice(0, 19);\n}\n\nfunction detectCardType(\n  cardNumber: string\n): \"visa\" | \"mastercard\" | \"amex\" | \"discover\" | \"other\" {\n  const cleaned = cardNumber.replace(/\\s+/g, \"\");\n  if (/^4/.test(cleaned)) return \"visa\";\n  if (/^5[1-5]/.test(cleaned)) return \"mastercard\";\n  if (/^3[47]/.test(cleaned)) return \"amex\";\n  if (/^6(?:011|5)/.test(cleaned)) return \"discover\";\n  return \"other\";\n}\n\nfunction getCardIcon(type: string): string {\n  switch (type) {\n    case \"visa\":\n      return \"💳\";\n    case \"mastercard\":\n      return \"💳\";\n    case \"amex\":\n      return \"💳\";\n    case \"discover\":\n      return \"💳\";\n    default:\n      return \"💳\";\n  }\n}\n\nexport default function BillingPaymentForm({\n  onSubmit,\n  onCancel,\n  defaultValues,\n  className,\n  isLoading = false,\n  errors,\n  showBillingAddress = false,\n  showSaveOption = true,\n  showSetDefault = false,\n}: BillingPaymentFormProps) {\n  const [cardNumber, setCardNumber] = useState(defaultValues?.cardNumber || \"\");\n  const [expiryMonth, setExpiryMonth] = useState(\n    defaultValues?.expiryMonth || \"\"\n  );\n  const [expiryYear, setExpiryYear] = useState(defaultValues?.expiryYear || \"\");\n  const [cvv, setCvv] = useState(defaultValues?.cvv || \"\");\n  const [holderName, setHolderName] = useState(defaultValues?.holderName || \"\");\n  const [showBilling, setShowBilling] = useState(showBillingAddress);\n  const [billingAddress, setBillingAddress] = useState(\n    defaultValues?.billingAddress || {\n      line1: \"\",\n      city: \"\",\n      state: \"\",\n      zip: \"\",\n      country: \"\",\n    }\n  );\n  const [saveForFuture, setSaveForFuture] = useState(\n    defaultValues?.saveForFuture ?? true\n  );\n  const [setAsDefault, setSetAsDefault] = useState(\n    defaultValues?.setAsDefault ?? false\n  );\n\n  const cardType = detectCardType(cardNumber);\n  const cardIcon = getCardIcon(cardType);\n\n  const handleCardNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const formatted = formatCardNumber(e.target.value);\n    setCardNumber(formatted);\n  };\n\n  const handleExpiryChange = (\n    e: React.ChangeEvent<HTMLInputElement>,\n    type: \"month\" | \"year\"\n  ) => {\n    const value = e.target.value.replace(/\\D/g, \"\");\n    if (type === \"month\") {\n      const month = Math.min(12, Math.max(1, Number.parseInt(value) || 0));\n      setExpiryMonth(month.toString().padStart(2, \"0\").slice(0, 2));\n    } else {\n      setExpiryYear(value.slice(0, 2));\n    }\n  };\n\n  const handleCvvChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const value = e.target.value.replace(/\\D/g, \"\");\n    const maxLength = cardType === \"amex\" ? 4 : 3;\n    setCvv(value.slice(0, maxLength));\n  };\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    onSubmit?.({\n      cardNumber: cardNumber.replace(/\\s+/g, \"\"),\n      expiryMonth,\n      expiryYear,\n      cvv,\n      holderName,\n      billingAddress: showBilling ? billingAddress : undefined,\n      saveForFuture,\n      setAsDefault,\n    });\n  };\n\n  const generalError = errors?.general;\n\n  return (\n    <Card className={cn(\"w-full shadow-xs\", className)}>\n      <CardHeader>\n        <div className=\"flex flex-col gap-1\">\n          <CardTitle className=\"flex items-center gap-2\">\n            <CreditCard className=\"size-5\" />\n            Payment method\n          </CardTitle>\n          <CardDescription>\n            Add a new payment method to your account\n          </CardDescription>\n        </div>\n      </CardHeader>\n      <CardContent>\n        <form className=\"flex flex-col gap-6\" onSubmit={handleSubmit}>\n          {generalError && (\n            <div\n              aria-live=\"polite\"\n              className=\"rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm\"\n              role=\"alert\"\n            >\n              {generalError}\n            </div>\n          )}\n\n          <Field data-invalid={!!errors?.cardNumber}>\n            <FieldLabel htmlFor=\"card-number\">\n              Card number\n              <span aria-label=\"required\" className=\"text-destructive\">\n                *\n              </span>\n            </FieldLabel>\n            <FieldContent>\n              <InputGroup\n                aria-describedby={\n                  errors?.cardNumber ? \"card-number-error\" : undefined\n                }\n                aria-invalid={!!errors?.cardNumber}\n              >\n                <InputGroupAddon>\n                  <span aria-hidden=\"true\" className=\"text-lg\">\n                    {cardIcon}\n                  </span>\n                </InputGroupAddon>\n                <InputGroupInput\n                  aria-describedby={\n                    errors?.cardNumber ? \"card-number-error\" : undefined\n                  }\n                  aria-invalid={!!errors?.cardNumber}\n                  id=\"card-number\"\n                  inputMode=\"numeric\"\n                  maxLength={19}\n                  onChange={handleCardNumberChange}\n                  placeholder=\"1234 5678 9012 3456\"\n                  type=\"text\"\n                  value={cardNumber}\n                />\n              </InputGroup>\n            </FieldContent>\n            {errors?.cardNumber && (\n              <FieldError id=\"card-number-error\">\n                {errors.cardNumber}\n              </FieldError>\n            )}\n          </Field>\n\n          <div className=\"grid grid-cols-2 gap-4\">\n            <Field data-invalid={!!errors?.expiry}>\n              <FieldLabel>\n                Expiry date\n                <span aria-label=\"required\" className=\"text-destructive\">\n                  *\n                </span>\n              </FieldLabel>\n              <FieldContent>\n                <div className=\"flex gap-2\">\n                  <div className=\"flex-1\">\n                    <label className=\"sr-only\" htmlFor=\"expiry-month\">\n                      Expiry month\n                    </label>\n                    <InputGroup\n                      aria-describedby={\n                        errors?.expiry ? \"expiry-error\" : undefined\n                      }\n                      aria-invalid={!!errors?.expiry}\n                    >\n                      <InputGroupInput\n                        aria-describedby={\n                          errors?.expiry ? \"expiry-error\" : undefined\n                        }\n                        aria-invalid={!!errors?.expiry}\n                        aria-label=\"Expiry month\"\n                        id=\"expiry-month\"\n                        inputMode=\"numeric\"\n                        maxLength={2}\n                        onChange={(e) => handleExpiryChange(e, \"month\")}\n                        placeholder=\"MM\"\n                        type=\"text\"\n                        value={expiryMonth}\n                      />\n                    </InputGroup>\n                  </div>\n                  <div className=\"flex-1\">\n                    <label className=\"sr-only\" htmlFor=\"expiry-year\">\n                      Expiry year\n                    </label>\n                    <InputGroup\n                      aria-describedby={\n                        errors?.expiry ? \"expiry-error\" : undefined\n                      }\n                      aria-invalid={!!errors?.expiry}\n                    >\n                      <InputGroupInput\n                        aria-describedby={\n                          errors?.expiry ? \"expiry-error\" : undefined\n                        }\n                        aria-invalid={!!errors?.expiry}\n                        aria-label=\"Expiry year\"\n                        id=\"expiry-year\"\n                        inputMode=\"numeric\"\n                        maxLength={2}\n                        onChange={(e) => handleExpiryChange(e, \"year\")}\n                        placeholder=\"YY\"\n                        type=\"text\"\n                        value={expiryYear}\n                      />\n                    </InputGroup>\n                  </div>\n                </div>\n              </FieldContent>\n              {errors?.expiry && (\n                <FieldError id=\"expiry-error\">{errors.expiry}</FieldError>\n              )}\n            </Field>\n\n            <Field data-invalid={!!errors?.cvv}>\n              <FieldLabel htmlFor=\"cvv\">\n                CVV\n                <span aria-label=\"required\" className=\"text-destructive\">\n                  *\n                </span>\n              </FieldLabel>\n              <FieldContent>\n                <InputGroup\n                  aria-describedby={errors?.cvv ? \"cvv-error\" : undefined}\n                  aria-invalid={!!errors?.cvv}\n                >\n                  <InputGroupInput\n                    aria-describedby={errors?.cvv ? \"cvv-error\" : undefined}\n                    aria-invalid={!!errors?.cvv}\n                    id=\"cvv\"\n                    inputMode=\"numeric\"\n                    maxLength={cardType === \"amex\" ? 4 : 3}\n                    onChange={handleCvvChange}\n                    placeholder={cardType === \"amex\" ? \"1234\" : \"123\"}\n                    type=\"text\"\n                    value={cvv}\n                  />\n                </InputGroup>\n              </FieldContent>\n              {errors?.cvv && (\n                <FieldError id=\"cvv-error\">{errors.cvv}</FieldError>\n              )}\n            </Field>\n          </div>\n\n          <Field data-invalid={!!errors?.holderName}>\n            <FieldLabel htmlFor=\"holder-name\">\n              Cardholder name\n              <span aria-label=\"required\" className=\"text-destructive\">\n                *\n              </span>\n            </FieldLabel>\n            <FieldContent>\n              <InputGroup\n                aria-describedby={\n                  errors?.holderName ? \"holder-name-error\" : undefined\n                }\n                aria-invalid={!!errors?.holderName}\n              >\n                <InputGroupInput\n                  aria-describedby={\n                    errors?.holderName ? \"holder-name-error\" : undefined\n                  }\n                  aria-invalid={!!errors?.holderName}\n                  id=\"holder-name\"\n                  onChange={(e) => setHolderName(e.target.value)}\n                  placeholder=\"John Doe\"\n                  type=\"text\"\n                  value={holderName}\n                />\n              </InputGroup>\n            </FieldContent>\n            {errors?.holderName && (\n              <FieldError id=\"holder-name-error\">\n                {errors.holderName}\n              </FieldError>\n            )}\n          </Field>\n\n          {showBillingAddress && (\n            <>\n              <Separator />\n              <div className=\"flex flex-col gap-4\">\n                <div className=\"flex items-center justify-between\">\n                  <h3 className=\"font-medium text-sm\">Billing address</h3>\n                  <Button\n                    onClick={() => setShowBilling(!showBilling)}\n                    type=\"button\"\n                    variant=\"ghost\"\n                  >\n                    {showBilling ? \"Hide\" : \"Show\"}\n                  </Button>\n                </div>\n                {showBilling && (\n                  <div className=\"flex flex-col gap-4\">\n                    <Field>\n                      <FieldLabel htmlFor=\"billing-line1\">\n                        Street address\n                      </FieldLabel>\n                      <FieldContent>\n                        <InputGroup>\n                          <InputGroupInput\n                            id=\"billing-line1\"\n                            onChange={(e) =>\n                              setBillingAddress({\n                                ...billingAddress,\n                                line1: e.target.value,\n                              })\n                            }\n                            placeholder=\"123 Main St\"\n                            type=\"text\"\n                            value={billingAddress.line1}\n                          />\n                        </InputGroup>\n                      </FieldContent>\n                    </Field>\n                    <div className=\"grid grid-cols-2 gap-4\">\n                      <Field>\n                        <FieldLabel htmlFor=\"billing-city\">City</FieldLabel>\n                        <FieldContent>\n                          <InputGroup>\n                            <InputGroupInput\n                              id=\"billing-city\"\n                              onChange={(e) =>\n                                setBillingAddress({\n                                  ...billingAddress,\n                                  city: e.target.value,\n                                })\n                              }\n                              placeholder=\"New York\"\n                              type=\"text\"\n                              value={billingAddress.city}\n                            />\n                          </InputGroup>\n                        </FieldContent>\n                      </Field>\n                      <Field>\n                        <FieldLabel htmlFor=\"billing-state\">State</FieldLabel>\n                        <FieldContent>\n                          <InputGroup>\n                            <InputGroupInput\n                              id=\"billing-state\"\n                              onChange={(e) =>\n                                setBillingAddress({\n                                  ...billingAddress,\n                                  state: e.target.value,\n                                })\n                              }\n                              placeholder=\"NY\"\n                              type=\"text\"\n                              value={billingAddress.state}\n                            />\n                          </InputGroup>\n                        </FieldContent>\n                      </Field>\n                    </div>\n                    <div className=\"grid grid-cols-2 gap-4\">\n                      <Field>\n                        <FieldLabel htmlFor=\"billing-zip\">ZIP code</FieldLabel>\n                        <FieldContent>\n                          <InputGroup>\n                            <InputGroupInput\n                              id=\"billing-zip\"\n                              onChange={(e) =>\n                                setBillingAddress({\n                                  ...billingAddress,\n                                  zip: e.target.value,\n                                })\n                              }\n                              placeholder=\"10001\"\n                              type=\"text\"\n                              value={billingAddress.zip}\n                            />\n                          </InputGroup>\n                        </FieldContent>\n                      </Field>\n                      <Field>\n                        <FieldLabel htmlFor=\"billing-country\">\n                          Country\n                        </FieldLabel>\n                        <FieldContent>\n                          <InputGroup>\n                            <InputGroupInput\n                              id=\"billing-country\"\n                              onChange={(e) =>\n                                setBillingAddress({\n                                  ...billingAddress,\n                                  country: e.target.value,\n                                })\n                              }\n                              placeholder=\"United States\"\n                              type=\"text\"\n                              value={billingAddress.country}\n                            />\n                          </InputGroup>\n                        </FieldContent>\n                      </Field>\n                    </div>\n                  </div>\n                )}\n              </div>\n            </>\n          )}\n\n          <Separator />\n\n          <div className=\"flex flex-col gap-3\">\n            <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n              <Shield className=\"size-3.5\" />\n              <span>Your payment information is encrypted and secure</span>\n            </div>\n            {showSaveOption && (\n              <div className=\"flex items-center gap-2\">\n                <Checkbox\n                  checked={saveForFuture}\n                  id=\"save-for-future\"\n                  onCheckedChange={(checked) =>\n                    setSaveForFuture(checked === true)\n                  }\n                />\n                <label className=\"text-sm\" htmlFor=\"save-for-future\">\n                  Save this card for future payments\n                </label>\n              </div>\n            )}\n            {showSetDefault && (\n              <div className=\"flex items-center gap-2\">\n                <Checkbox\n                  checked={setAsDefault}\n                  id=\"set-as-default\"\n                  onCheckedChange={(checked) =>\n                    setSetAsDefault(checked === true)\n                  }\n                />\n                <label className=\"text-sm\" htmlFor=\"set-as-default\">\n                  Set as default payment method\n                </label>\n              </div>\n            )}\n          </div>\n\n          <div className=\"flex flex-col gap-2 sm:flex-row\">\n            {onCancel && (\n              <Button\n                className=\"w-full sm:w-auto\"\n                onClick={onCancel}\n                type=\"button\"\n                variant=\"outline\"\n              >\n                Cancel\n              </Button>\n            )}\n            <Button\n              aria-busy={isLoading}\n              className=\"w-full sm:w-auto\"\n              data-loading={isLoading}\n              type=\"submit\"\n            >\n              {isLoading ? (\n                <>\n                  <Loader2 className=\"size-4 animate-spin\" />\n                  Processing…\n                </>\n              ) : (\n                <>\n                  <Lock className=\"size-4\" />\n                  Add payment method\n                </>\n              )}\n            </Button>\n          </div>\n        </form>\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "billing"
  ],
  "type": "registry:ui"
}