{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "payment-details-two",
  "title": "Payment Details Two",
  "description": "A comprehensive payment details form component with card details, billing information, and country selection built using React Hook Form and custom validation",
  "dependencies": [
    "lucide-react",
    "react-hook-form",
    "motion",
    "country-state-city"
  ],
  "registryDependencies": [
    "button",
    "card",
    "input",
    "label",
    "select",
    "textarea",
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/billingsdk/payment-details-two.tsx",
      "content": "\"use client\";\n\nimport { useForm } from \"react-hook-form\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  detectCardType,\n  formatCardNumber,\n  formatExpiryDate,\n  validateLuhn,\n} from \"@/utils/card-validation\";\nimport {\n  Check,\n  CreditCard,\n  Shield,\n  ChevronLeft,\n  ChevronRight,\n} from \"lucide-react\";\nimport { useState, useEffect } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { Country, State, City } from \"country-state-city\";\n\nexport interface PaymentFormData {\n  nameOnCard?: string;\n  cardNumber?: string;\n  validTill?: string;\n  cvv?: string;\n  firstName?: string;\n  middleLastName?: string;\n  country?: string;\n  state?: string;\n  city?: string;\n  billingAddress?: string;\n  pinCode?: string;\n  contactNumber?: string;\n  general?: string;\n}\n\nconst CardLogo = ({ type }: { type: string }) => {\n  switch (type) {\n    case \"visa\":\n      return (\n        <div className=\"flex h-5 w-10 items-center justify-center rounded bg-blue-600 text-xs font-bold text-white\">\n          VISA\n        </div>\n      );\n    case \"mastercard\":\n      return (\n        <div className=\"flex items-center\">\n          <div className=\"h-5 w-5 rounded-full bg-red-500\"></div>\n          <div className=\"-ml-2 h-5 w-5 rounded-full bg-orange-400\"></div>\n        </div>\n      );\n    case \"amex\":\n      return (\n        <div className=\"flex h-6 w-10 items-center justify-center rounded bg-blue-500 text-xs font-bold text-white\">\n          AMEX\n        </div>\n      );\n    case \"rupay\":\n      return (\n        <div className=\"flex h-5 w-10 items-center justify-center rounded bg-green-600 text-xs font-bold text-white\">\n          RuPay\n        </div>\n      );\n    case \"discover\":\n      return (\n        <div className=\"flex h-6 w-10 items-center justify-center rounded bg-orange-600 text-xs font-bold text-white\">\n          DISC\n        </div>\n      );\n    default:\n      return <CreditCard className=\"text-muted-foreground h-5 w-5\" />;\n  }\n};\n\nexport function PaymentDetailsTwo({\n  className,\n  onSubmit,\n  onDiscard,\n  countries,\n  states,\n  cities,\n}: {\n  className?: string;\n  onSubmit?: (data: PaymentFormData) => void;\n  onDiscard?: () => void;\n  countries?: { name: string; isoCode: string }[];\n  states?: { name: string; isoCode: string }[];\n  cities?: { name: string }[];\n}) {\n  const [step, setStep] = useState(1);\n  const [cardType, setCardType] = useState(\"\");\n  const [isSaved, setIsSaved] = useState(false);\n  const [defaultCountries, setDefaultCountries] = useState<\n    { name: string; isoCode: string }[]\n  >([]);\n  const [defaultStates, setDefaultStates] = useState<\n    { name: string; isoCode: string }[]\n  >([]);\n  const [defaultCities, setDefaultCities] = useState<{ name: string }[]>([]);\n  const [selectedCountry, setSelectedCountry] = useState<string>(\"\");\n  const [selectedState, setSelectedState] = useState<string>(\"\");\n\n  const {\n    register,\n    handleSubmit,\n    setValue,\n    reset,\n    formState: { errors, isSubmitting },\n  } = useForm<PaymentFormData>({\n    defaultValues: {\n      nameOnCard: \"\",\n      cardNumber: \"\",\n      validTill: \"\",\n      cvv: \"\",\n      firstName: \"\",\n      middleLastName: \"\",\n      country: \"\",\n      state: \"\",\n      city: \"\",\n      billingAddress: \"\",\n      pinCode: \"\",\n      contactNumber: \"\",\n    },\n  });\n\n  useEffect(() => {\n    // fetch all countries\n    if (countries && countries?.length > 0) {\n      setDefaultCountries(countries);\n    } else {\n      const countryData = Country.getAllCountries();\n      setDefaultCountries(countryData);\n    }\n  }, []);\n\n  useEffect(() => {\n    if (states && states.length > 0) {\n      setDefaultStates(states);\n    } else {\n      const stateData = State.getStatesOfCountry(selectedCountry);\n      setDefaultStates(stateData);\n    }\n  }, [selectedCountry]);\n\n  useEffect(() => {\n    if (cities && cities.length > 0) {\n      setDefaultCities(cities);\n    } else {\n      const cityData = City.getCitiesOfState(selectedCountry, selectedState);\n      setDefaultCities(cityData);\n    }\n  }, [selectedCountry, selectedState]);\n\n  const handleFormSubmit = async (data: PaymentFormData) => {\n    if (!onSubmit) return;\n    try {\n      await onSubmit(data);\n      setIsSaved(true);\n    } catch (err) {\n      console.error(err);\n    }\n  };\n\n  const handleDiscardClick = () => {\n    if (onDiscard) {\n      onDiscard();\n    } else {\n      reset();\n      setIsSaved(false);\n    }\n  };\n\n  const formatAndSetCard = (val: string) => {\n    const raw = val.replace(/\\s+/g, \"\");\n    const formatted = formatCardNumber(raw);\n    setValue(\"cardNumber\", formatted);\n    setCardType(detectCardType(formatted));\n  };\n\n  const handleNext = () => {\n    setStep(2);\n  };\n\n  const handleBack = () => {\n    setStep(1);\n  };\n\n  return (\n    <Card className={cn(\"mx-auto w-full max-w-2xl pb-0\", className)}>\n      <CardHeader className=\"space-y-4\">\n        <div>\n          <CardTitle>Payment Details</CardTitle>\n          <CardDescription className=\"mt-1.5\">\n            {step === 1\n              ? \"Enter your card information\"\n              : \"Enter your billing address\"}\n          </CardDescription>\n        </div>\n\n        {/* Progress Indicator */}\n        <div className=\"flex items-center gap-2\">\n          <div className=\"flex flex-1 items-center gap-2\">\n            <div\n              className={cn(\n                \"flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium transition-colors\",\n                step === 1\n                  ? \"bg-primary text-primary-foreground\"\n                  : \"bg-primary/20 text-primary\",\n              )}\n            >\n              1\n            </div>\n            <div className=\"bg-border h-1 flex-1 rounded-full\">\n              <div\n                className={cn(\n                  \"bg-primary h-full rounded-full transition-all duration-300\",\n                  step === 2 ? \"w-full\" : \"w-0\",\n                )}\n              />\n            </div>\n          </div>\n          <div\n            className={cn(\n              \"flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium transition-colors\",\n              step === 2\n                ? \"bg-primary text-primary-foreground\"\n                : \"bg-muted text-muted-foreground\",\n            )}\n          >\n            2\n          </div>\n        </div>\n      </CardHeader>\n\n      <form onSubmit={handleSubmit(handleFormSubmit)}>\n        <CardContent className=\"min-h-[420px] py-6\">\n          <AnimatePresence mode=\"wait\">\n            {step === 1 ? (\n              <motion.div\n                key=\"step1\"\n                initial={{ opacity: 0, x: 20 }}\n                animate={{ opacity: 1, x: 0 }}\n                exit={{ opacity: 0, x: -20 }}\n                transition={{ duration: 0.3 }}\n                className=\"space-y-5\"\n              >\n                <div className=\"space-y-2\">\n                  <Label htmlFor=\"nameOnCard\">Name on card</Label>\n                  <Input\n                    id=\"nameOnCard\"\n                    placeholder=\"John Doe\"\n                    {...register(\"nameOnCard\", {\n                      required: \"Name is required\",\n                    })}\n                  />\n                  {errors.nameOnCard && (\n                    <p className=\"text-destructive text-sm\">\n                      {errors.nameOnCard.message}\n                    </p>\n                  )}\n                </div>\n\n                <div className=\"space-y-2\">\n                  <Label htmlFor=\"cardNumber\">Card number</Label>\n                  <div className=\"relative\">\n                    <div className=\"absolute top-1/2 left-3 z-10 -translate-y-1/2\">\n                      <CardLogo type={cardType} />\n                    </div>\n                    <Input\n                      id=\"cardNumber\"\n                      className=\"pl-14 font-mono tracking-wide\"\n                      placeholder=\"1234 5678 9012 3456\"\n                      maxLength={20}\n                      inputMode=\"numeric\"\n                      pattern=\"[0-9 ]*\"\n                      {...register(\"cardNumber\", {\n                        required: \"Card number is required\",\n                        validate: (val) =>\n                          val ? validateLuhn(val) : \"Invalid card number\",\n                      })}\n                      onChange={(e) => formatAndSetCard(e.target.value)}\n                    />\n                  </div>\n                  {errors.cardNumber && (\n                    <p className=\"text-destructive text-sm\">\n                      {errors.cardNumber.message}\n                    </p>\n                  )}\n                </div>\n\n                <div className=\"grid grid-cols-2 gap-4\">\n                  <div className=\"space-y-2\">\n                    <Label htmlFor=\"validTill\">Expiry date</Label>\n                    <Input\n                      id=\"validTill\"\n                      className=\"font-mono\"\n                      placeholder=\"MM/YY\"\n                      maxLength={5}\n                      {...register(\"validTill\", {\n                        required: \"Valid expiry date is required (MM/YY)\",\n                        validate: (val?: string) => {\n                          if (!val)\n                            return \"Valid expiry date is required (MM/YY)\";\n                          if (!/^\\d{2}\\/\\d{2}$/.test(val))\n                            return \"Invalid format (MM/YY)\";\n                          const [mm, yy] = val.split(\"/\").map(Number);\n                          if (mm < 1 || mm > 12) return \"Invalid month\";\n                          const now = new Date();\n                          const firstInvalidDay = new Date(2000 + yy, mm, 1);\n                          if (firstInvalidDay <= now) return \"Card expired\";\n                          return true;\n                        },\n                      })}\n                      onChange={(e) =>\n                        setValue(\"validTill\", formatExpiryDate(e.target.value))\n                      }\n                    />\n                    {errors.validTill && (\n                      <p className=\"text-destructive text-sm\">\n                        {errors.validTill.message}\n                      </p>\n                    )}\n                  </div>\n                  <div className=\"space-y-2\">\n                    <Label htmlFor=\"cvv\">CVV</Label>\n                    <div className=\"relative\">\n                      <Shield className=\"text-muted-foreground absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2\" />\n                      <Input\n                        id=\"cvv\"\n                        type=\"password\"\n                        className=\"pl-10 font-mono\"\n                        placeholder=\"123\"\n                        maxLength={4}\n                        inputMode=\"numeric\"\n                        pattern=\"[0-9 ]*\"\n                        {...register(\"cvv\", {\n                          required: \"Valid CVV is required\",\n                          validate: (val) =>\n                            (cardType === \"amex\"\n                              ? val?.length === 4\n                              : val?.length === 3) || \"Invalid CVV length\",\n                        })}\n                      />\n                    </div>\n                    {errors.cvv && (\n                      <p className=\"text-destructive text-sm\">\n                        {errors.cvv.message}\n                      </p>\n                    )}\n                  </div>\n                </div>\n              </motion.div>\n            ) : (\n              <motion.div\n                key=\"step2\"\n                initial={{ opacity: 0, x: 20 }}\n                animate={{ opacity: 1, x: 0 }}\n                exit={{ opacity: 0, x: -20 }}\n                transition={{ duration: 0.3 }}\n                className=\"space-y-5\"\n              >\n                <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n                  <div className=\"space-y-2\">\n                    <Label htmlFor=\"firstName\">First name</Label>\n                    <Input\n                      id=\"firstName\"\n                      placeholder=\"John\"\n                      {...register(\"firstName\", { required: \"Required\" })}\n                    />\n                    {errors.firstName && (\n                      <p className=\"text-destructive text-sm\">\n                        {errors.firstName.message}\n                      </p>\n                    )}\n                  </div>\n                  <div className=\"space-y-2\">\n                    <Label htmlFor=\"middleLastName\">Last name</Label>\n                    <Input\n                      id=\"middleLastName\"\n                      placeholder=\"Doe\"\n                      {...register(\"middleLastName\", { required: \"Required\" })}\n                    />\n                    {errors.middleLastName && (\n                      <p className=\"text-destructive text-sm\">\n                        {errors.middleLastName.message}\n                      </p>\n                    )}\n                  </div>\n                </div>\n\n                <div className=\"grid grid-cols-1 gap-4 md:grid-cols-3\">\n                  <div className=\"space-y-2\">\n                    <Label htmlFor=\"country\">Country</Label>\n                    <Select\n                      onValueChange={(val) => {\n                        setValue(\"country\", val);\n                        setSelectedCountry(val);\n                      }}\n                    >\n                      <SelectTrigger id=\"country\">\n                        <SelectValue placeholder=\"Select country\" />\n                      </SelectTrigger>\n                      <SelectContent>\n                        {defaultCountries.map((c) => (\n                          <SelectItem key={c.isoCode} value={c.isoCode}>\n                            {c.name}\n                          </SelectItem>\n                        ))}\n                      </SelectContent>\n                    </Select>\n                  </div>\n\n                  <div className=\"space-y-2\">\n                    <Label htmlFor=\"state\">State</Label>\n                    <Select\n                      onValueChange={(val) => {\n                        setValue(\"state\", val);\n                        setSelectedState(val);\n                      }}\n                    >\n                      <SelectTrigger id=\"state\">\n                        <SelectValue placeholder=\"Select state\" />\n                      </SelectTrigger>\n                      <SelectContent>\n                        {defaultStates.length > 0 ? (\n                          defaultStates.map((s) => (\n                            <SelectItem key={s.isoCode} value={s.isoCode}>\n                              {s.name}{\" \"}\n                            </SelectItem>\n                          ))\n                        ) : (\n                          <SelectItem disabled value=\"No state found\">\n                            No state found\n                          </SelectItem>\n                        )}\n                      </SelectContent>\n                    </Select>\n                  </div>\n\n                  <div className=\"space-y-2\">\n                    <Label htmlFor=\"city\">City</Label>\n                    <Select onValueChange={(val) => setValue(\"city\", val)}>\n                      <SelectTrigger id=\"city\">\n                        <SelectValue placeholder=\"Select city\" />\n                      </SelectTrigger>\n                      <SelectContent>\n                        {defaultCities.length > 0 ? (\n                          defaultCities.map((c) => (\n                            <SelectItem key={c.name} value={c.name}>\n                              {c.name}\n                            </SelectItem>\n                          ))\n                        ) : (\n                          <SelectItem disabled value=\"No city found\">\n                            No city found\n                          </SelectItem>\n                        )}\n                      </SelectContent>\n                    </Select>\n                  </div>\n                </div>\n\n                <div className=\"space-y-2\">\n                  <Label htmlFor=\"billingAddress\">Billing Address</Label>\n                  <Textarea\n                    id=\"billingAddress\"\n                    placeholder=\"Enter Billing Address\"\n                    {...register(\"billingAddress\", { required: \"Required\" })}\n                  />\n                  {errors.billingAddress && (\n                    <p className=\"text-destructive text-sm\">\n                      {errors.billingAddress.message}\n                    </p>\n                  )}\n                </div>\n\n                <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n                  <div className=\"space-y-2\">\n                    <Label htmlFor=\"pinCode\">Pincode</Label>\n                    <Input\n                      id=\"pinCode\"\n                      placeholder=\"110024\"\n                      {...register(\"pinCode\", {\n                        required: \"Required\",\n                        pattern: {\n                          value: /^[0-9]{6}$/,\n                          message: \"Invalid pincode\",\n                        },\n                      })}\n                    />\n                    {errors.pinCode && (\n                      <p className=\"text-destructive text-sm\">\n                        {errors.pinCode.message}\n                      </p>\n                    )}\n                  </div>\n\n                  <div className=\"space-y-2\">\n                    <Label htmlFor=\"contactNumber\">Mobile</Label>\n                    <Input\n                      id=\"contactNumber\"\n                      placeholder=\"9991023558\"\n                      {...register(\"contactNumber\", {\n                        required: \"Required\",\n                        pattern: {\n                          value: /^[0-9]{10}$/,\n                          message: \"Invalid number\",\n                        },\n                      })}\n                    />\n                    {errors.contactNumber && (\n                      <p className=\"text-destructive text-sm\">\n                        {errors.contactNumber.message}\n                      </p>\n                    )}\n                  </div>\n                </div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </CardContent>\n\n        <CardFooter className=\"bg-muted/30 flex justify-between border-t py-6\">\n          {step === 1 ? (\n            <>\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                onClick={handleDiscardClick}\n              >\n                Cancel\n              </Button>\n              <Button\n                type=\"button\"\n                onClick={handleNext}\n                className=\"min-w-[100px]\"\n              >\n                Next\n                <ChevronRight className=\"ml-2 h-4 w-4\" />\n              </Button>\n            </>\n          ) : (\n            <>\n              <Button type=\"button\" variant=\"ghost\" onClick={handleBack}>\n                <ChevronLeft className=\"mr-2 h-4 w-4\" />\n                Back\n              </Button>\n              <Button\n                type=\"submit\"\n                disabled={isSubmitting || isSaved}\n                className=\"min-w-[120px]\"\n              >\n                <AnimatePresence mode=\"wait\" initial={false}>\n                  {isSubmitting ? (\n                    <motion.div\n                      key=\"saving\"\n                      initial={{ opacity: 0 }}\n                      animate={{ opacity: 1 }}\n                      exit={{ opacity: 0 }}\n                      className=\"flex items-center gap-2\"\n                    >\n                      <div className=\"border-background h-4 w-4 animate-spin rounded-full border-2 border-t-transparent\" />\n                      <span>Saving...</span>\n                    </motion.div>\n                  ) : isSaved ? (\n                    <motion.div\n                      key=\"saved\"\n                      initial={{ scale: 0 }}\n                      animate={{ scale: 1 }}\n                      exit={{ scale: 0 }}\n                      className=\"flex items-center gap-2\"\n                    >\n                      <Check className=\"h-4 w-4\" />\n                      <span>Saved</span>\n                    </motion.div>\n                  ) : (\n                    <motion.span\n                      key=\"default\"\n                      initial={{ opacity: 0 }}\n                      animate={{ opacity: 1 }}\n                      exit={{ opacity: 0 }}\n                    >\n                      Save Changes\n                    </motion.span>\n                  )}\n                </AnimatePresence>\n              </Button>\n            </>\n          )}\n        </CardFooter>\n      </form>\n    </Card>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/billingsdk/payment-details-two.tsx"
    },
    {
      "path": "src/registry/billingsdk/demo/payment-details-two-demo.tsx",
      "content": "\"use client\";\n\nimport {\n  PaymentDetailsTwo,\n  type PaymentFormData,\n} from \"@/components/billingsdk/payment-details-two\";\n\nexport function PaymentDetailsTwoDemo() {\n  //Function that handles onSubmit in the Form\n  const handleSubmit = async (_data: PaymentFormData) => {\n    // _data contains all the form values entered by the user:\n    // e.g., _data.cardNumber, _data.nameOnCard, _data.validTill, _data.cvv, _data.firstName, etc.\n    // You can use this object to make an API call to your backend, for example:\n    // await fetch('/api/payment', { method: 'POST', body: JSON.stringify(_data) })\n\n    return await new Promise((resolve) => {\n      setTimeout(() => {\n        console.log(\"Handle Submit Function\");\n        resolve(\"The promise is resolved\");\n      }, 3000);\n    });\n  };\n\n  //Function that handles onCancel in the Form\n  const handleDiscard = () => {\n    console.log(\"The Discard Function\");\n  };\n\n  return (\n    <PaymentDetailsTwo onSubmit={handleSubmit} onDiscard={handleDiscard} />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/payment-details-two-demo.tsx"
    },
    {
      "path": "src/utils/card-validation.ts",
      "content": "// Detect card type\nexport const detectCardType = (\n  cardNumber: string,\n):\n  | \"visa\"\n  | \"mastercard\"\n  | \"amex\"\n  | \"rupay\"\n  | \"diners\"\n  | \"discover\"\n  | \"unknown\" => {\n  const number = cardNumber.replace(/\\s/g, \"\");\n\n  if (/^4/.test(number)) return \"visa\";\n  if (/^3[47]/.test(number)) return \"amex\";\n  if (/^3(?:0[0-5]|09|095|6|8)/.test(number)) return \"diners\";\n  if (\n    /^5[1-5]/.test(number) ||\n    /^222[1-9]/.test(number) ||\n    /^22[3-9]\\d/.test(number) ||\n    /^2[3-6]\\d{2}/.test(number) ||\n    /^27[0-1]\\d/.test(number) ||\n    /^2720/.test(number)\n  )\n    return \"mastercard\";\n  if (\n    /^6011/.test(number) ||\n    /^65/.test(number) ||\n    /^64[4-9]/.test(number) ||\n    /^622(?:12[6-9]|1[3-9]\\d|[2-8]\\d{2}|9[0-1]\\d|92[0-5])/.test(number)\n  )\n    return \"discover\";\n  if (\n    /^60/.test(number) ||\n    /^81/.test(number) ||\n    /^82/.test(number) ||\n    /^508/.test(number)\n  )\n    return \"rupay\";\n\n  return \"unknown\";\n};\n\n// Luhn algorithm\nexport const validateLuhn = (cardNumber: string): true | string => {\n  const number = cardNumber.replace(/\\s/g, \"\");\n  if (!number || !/^\\d+$/.test(number)) return \"Invalid card number\";\n\n  let sum = 0;\n  let shouldDouble = false;\n\n  for (let i = number.length - 1; i >= 0; i--) {\n    let digit = parseInt(number.charAt(i));\n    if (shouldDouble) {\n      digit *= 2;\n      if (digit > 9) digit -= 9;\n    }\n    sum += digit;\n    shouldDouble = !shouldDouble;\n  }\n\n  return sum % 10 === 0 ? true : \"Invalid card number\";\n};\n\n/**\n * Formats a card number by adding spaces every 4 digits\n * @param value - The raw card number input\n * @returns The formatted card number with spaces\n */\nexport const formatCardNumber = (value: string): string => {\n  const number = value.replace(/\\s/g, \"\").replace(/[^0-9]/gi, \"\");\n  const matches = number.match(/\\d{4,16}/g);\n  const match = (matches && matches[0]) || \"\";\n  const parts = [];\n\n  for (let i = 0, len = match.length; i < len; i += 4) {\n    parts.push(match.substring(i, i + 4));\n  }\n\n  if (parts.length) {\n    return parts.join(\" \");\n  } else {\n    return number;\n  }\n};\n\n/**\n * Formats an expiry date of the card\n * @param value - The raw card expiry input\n * @returns The formatted expiry date with / in between\n */\nexport const formatExpiryDate = (value?: string): string => {\n  if (!value) return \"\";\n  const number = value.replace(/\\D/g, \"\");\n  if (number.length >= 2) {\n    return number.substring(0, 2) + \"/\" + number.substring(2, 4);\n  }\n  return number;\n};\n",
      "type": "registry:component",
      "target": "utils/card-validation.ts"
    }
  ],
  "type": "registry:block"
}