{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "card-input",
  "type": "registry:ui",
  "title": "Card Input",
  "description": "Payment card fields with brand detection, live formatting, Luhn checks and a card preview that flips for the CVC.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/card-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion } from \"motion/react\";\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", callback);\n  return () => mq.removeEventListener(\"change\", callback);\n}\n\n/** SSR-safe: hydrates as \"no preference\", then follows the media query. */\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Card rules                                                                */\n/* -------------------------------------------------------------------------- */\n\nexport type CardBrand = \"visa\" | \"mastercard\" | \"amex\" | \"discover\";\nexport type CardField = \"number\" | \"expiry\" | \"cvc\" | \"name\";\n\nexport interface CardInputValue {\n  /** Card number digits only (no spaces). */\n  number: string;\n  /** Brand detected from the IIN prefix, or null while unknown. */\n  brand: CardBrand | null;\n  /** 1–12, or null until two digits are typed. */\n  expiryMonth: number | null;\n  /** Four-digit year, or null until complete. */\n  expiryYear: number | null;\n  cvc: string;\n  name: string;\n  /** Validation messages for fields that are currently invalid (empty fields included). */\n  errors: Partial<Record<CardField, string>>;\n  /** True when every field is valid. */\n  complete: boolean;\n}\n\ntype BrandRule = { label: string; lengths: number[]; gaps: number[]; cvc: number };\n\nconst BRANDS: Record<CardBrand, BrandRule> = {\n  visa: { label: \"Visa\", lengths: [13, 16, 19], gaps: [4, 8, 12, 16], cvc: 3 },\n  mastercard: { label: \"Mastercard\", lengths: [16], gaps: [4, 8, 12], cvc: 3 },\n  amex: { label: \"American Express\", lengths: [15], gaps: [4, 10], cvc: 4 },\n  discover: { label: \"Discover\", lengths: [16, 19], gaps: [4, 8, 12, 16], cvc: 3 },\n};\nconst UNKNOWN: BrandRule = { label: \"Card\", lengths: [16], gaps: [4, 8, 12, 16], cvc: 3 };\nconst MAX_UNKNOWN = 19;\n\n/** Detects the brand from the issuer identification number (IIN) prefix. */\nexport function detectCardBrand(digits: string): CardBrand | null {\n  if (/^4/.test(digits)) return \"visa\";\n  if (/^3[47]/.test(digits)) return \"amex\";\n  if (/^5[1-5]/.test(digits)) return \"mastercard\";\n  if (digits.length >= 4) {\n    const p4 = Number(digits.slice(0, 4));\n    if (p4 >= 2221 && p4 <= 2720) return \"mastercard\";\n  }\n  if (/^(6011|65|64[4-9])/.test(digits)) return \"discover\";\n  if (digits.length >= 6) {\n    const p6 = Number(digits.slice(0, 6));\n    if (p6 >= 622126 && p6 <= 622925) return \"discover\";\n  }\n  return null;\n}\n\n/** Luhn (mod 10) checksum. */\nexport function luhnCheck(digits: string): boolean {\n  if (!/^\\d+$/.test(digits)) return false;\n  let sum = 0;\n  for (let i = 0; i < digits.length; i++) {\n    let d = Number(digits[digits.length - 1 - i]);\n    if (i % 2 === 1) {\n      d *= 2;\n      if (d > 9) d -= 9;\n    }\n    sum += d;\n  }\n  return sum % 10 === 0;\n}\n\nconst ruleFor = (brand: CardBrand | null) => (brand ? BRANDS[brand] : UNKNOWN);\nconst maxLength = (brand: CardBrand | null) => (brand ? Math.max(...BRANDS[brand].lengths) : MAX_UNKNOWN);\n\nfunction formatNumber(digits: string, brand: CardBrand | null) {\n  const { gaps } = ruleFor(brand);\n  let out = \"\";\n  for (let i = 0; i < digits.length; i++) {\n    if (i > 0 && gaps.includes(i)) out += \" \";\n    out += digits[i];\n  }\n  return out;\n}\n\n/** Caret index in the formatted string that sits right after `count` digits. */\nconst caretAfter = (count: number, brand: CardBrand | null) =>\n  count + ruleFor(brand).gaps.filter((g) => g > 0 && g < count).length;\n\nconst expiryDigits = (text: string) => text.replace(/\\D/g, \"\").slice(0, 4);\n\nfunction formatExpiry(digits: string, inserting: boolean) {\n  if (digits.length < 2) return digits;\n  if (digits.length === 2) return inserting ? `${digits} / ` : digits;\n  return `${digits.slice(0, 2)} / ${digits.slice(2)}`;\n}\n\ntype Today = { year: number; month: number };\n\nfunction validate(\n  fields: Record<CardField, string>,\n  requireName: boolean,\n  today: Today | null,\n): Partial<Record<CardField, string>> {\n  const errors: Partial<Record<CardField, string>> = {};\n  const brand = detectCardBrand(fields.number);\n  const rule = ruleFor(brand);\n\n  if (!fields.number) errors.number = \"Enter a card number.\";\n  else if (!rule.lengths.includes(fields.number.length) && fields.number.length < maxLength(brand))\n    errors.number = \"Card number is incomplete.\";\n  else if (!rule.lengths.includes(fields.number.length) || !luhnCheck(fields.number)) errors.number = \"Card number is invalid.\";\n\n  const exp = expiryDigits(fields.expiry);\n  const month = Number(exp.slice(0, 2));\n  if (!exp) errors.expiry = \"Enter an expiry date.\";\n  else if (exp.length >= 2 && (month < 1 || month > 12)) errors.expiry = \"Month must be between 01 and 12.\";\n  else if (exp.length < 4) errors.expiry = \"Expiry date is incomplete.\";\n  else if (today) {\n    const year = 2000 + Number(exp.slice(2));\n    if (year < today.year || (year === today.year && month < today.month)) errors.expiry = \"This card has expired.\";\n    else if (year > today.year + 20) errors.expiry = \"Expiry year is too far ahead.\";\n  }\n\n  if (!fields.cvc) errors.cvc = \"Enter the security code.\";\n  else if (fields.cvc.length !== rule.cvc) errors.cvc = `Security code is ${rule.cvc} digits.`;\n\n  if (requireName && !fields.name.trim()) errors.name = \"Enter the name on the card.\";\n  return errors;\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Component                                                                 */\n/* -------------------------------------------------------------------------- */\n\nexport type CardPreviewStyle = \"graphite\" | \"accent\" | \"paper\";\n\nexport interface CardInputProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\" | \"children\"> {\n  /** Called after every edit with structured, validated data. Nothing is sent or stored by the component. */\n  onChange?: (value: CardInputValue) => void;\n  /** Show the animated card preview. */\n  showPreview?: boolean;\n  /** Flip the preview to its back while the security code is focused (Amex keeps its code on the front). */\n  flipOnCvc?: boolean;\n  /** Move focus to the next field once a field is complete and valid. */\n  autoAdvance?: boolean;\n  /** Treat the cardholder name as required. */\n  requireName?: boolean;\n  /** Preview card finish. */\n  cardStyle?: CardPreviewStyle;\n  /** Focus ring, caret and accent-finish colour (any CSS colour). */\n  accent?: string;\n  /** Placeholder (and hint) for the number field — keep it a public test number. */\n  numberPlaceholder?: string;\n  disabled?: boolean;\n}\n\nfunction BrandMark({ brand, className = \"\" }: { brand: CardBrand | null; className?: string }) {\n  // Generic text badges — deliberately not the networks' trademarked logos.\n  if (!brand) return null;\n  const base = `inline-flex select-none items-center leading-none ${className}`;\n  switch (brand) {\n    case \"visa\":\n      return <span className={`${base} text-[1.05em] font-black italic tracking-[-0.04em]`}>VISA</span>;\n    case \"mastercard\":\n      return <span className={`${base} text-[0.62em] font-bold uppercase tracking-[0.12em]`}>Mastercard</span>;\n    case \"amex\":\n      return <span className={`${base} rounded-[3px] border border-current px-1 py-0.5 text-[0.62em] font-bold tracking-[0.1em]`}>AMEX</span>;\n    case \"discover\":\n      return <span className={`${base} text-[0.62em] font-bold uppercase tracking-[0.18em]`}>Discover</span>;\n  }\n}\n\n/**\n * Payment card entry UI — interface only. It formats, detects the brand by IIN prefix, validates\n * with Luhn and expiry rules and reports structured data through `onChange`; it never transmits or\n * stores anything. Use a PCI-compliant processor (hosted fields / tokenisation) for real payments.\n */\nexport function CardInput({\n  onChange,\n  showPreview = true,\n  flipOnCvc = true,\n  autoAdvance = true,\n  requireName = true,\n  cardStyle = \"graphite\",\n  accent = \"#FF7448\",\n  numberPlaceholder = \"4242 4242 4242 4242\",\n  disabled = false,\n  className = \"\",\n  style,\n  ...props\n}: CardInputProps) {\n  const reduceMotion = usePrefersReducedMotion();\n  const uid = React.useId();\n  const id = (f: string) => `ci-${uid}-${f}`;\n\n  const [number, setNumber] = React.useState(\"\");\n  const [expiry, setExpiry] = React.useState(\"\");\n  const [cvc, setCvc] = React.useState(\"\");\n  const [name, setName] = React.useState(\"\");\n  const [touched, setTouched] = React.useState<Record<CardField, boolean>>({ number: false, expiry: false, cvc: false, name: false });\n  const [focused, setFocused] = React.useState<CardField | null>(null);\n  // Today's month is captured on first focus (an event), keeping render pure and SSR-safe.\n  const [today, setToday] = React.useState<Today | null>(null);\n\n  const numberRef = React.useRef<HTMLInputElement>(null);\n  const expiryRef = React.useRef<HTMLInputElement>(null);\n  const cvcRef = React.useRef<HTMLInputElement>(null);\n  const nameRef = React.useRef<HTMLInputElement>(null);\n  const pendingCaret = React.useRef<number | null>(null);\n\n  const brand = detectCardBrand(number);\n  const rule = ruleFor(brand);\n  const formattedNumber = formatNumber(number, brand);\n  const fields = { number, expiry, cvc, name };\n  const errors = validate(fields, requireName, today);\n  const expDigits = expiryDigits(expiry);\n\n  // Errors appear after a field is left, or as soon as a full-length entry is wrong.\n  const visible: Partial<Record<CardField, string>> = {};\n  if (errors.number && (touched.number || number.length >= maxLength(brand) || (number.length === 16 && brand !== \"amex\")))\n    visible.number = errors.number;\n  if (errors.expiry && (touched.expiry || expDigits.length === 4 || (expDigits.length >= 2 && Number(expDigits.slice(0, 2)) > 12)))\n    visible.expiry = errors.expiry;\n  if (errors.cvc && touched.cvc) visible.cvc = errors.cvc;\n  if (errors.name && touched.name) visible.name = errors.name;\n\n  const emit = (patch: Partial<Record<CardField, string>>) => {\n    if (!onChange) return;\n    const next = { ...fields, ...patch };\n    const nextErrors = validate(next, requireName, today);\n    const exp = expiryDigits(next.expiry);\n    const month = exp.length >= 2 ? Number(exp.slice(0, 2)) : null;\n    onChange({\n      number: next.number,\n      brand: detectCardBrand(next.number),\n      expiryMonth: month && month >= 1 && month <= 12 ? month : null,\n      expiryYear: exp.length === 4 ? 2000 + Number(exp.slice(2)) : null,\n      cvc: next.cvc,\n      name: next.name,\n      errors: nextErrors,\n      complete: Object.keys(nextErrors).length === 0,\n    });\n  };\n\n  // Restore the caret after reformatting the card number (spaces shift it otherwise).\n  React.useLayoutEffect(() => {\n    const pos = pendingCaret.current;\n    const el = numberRef.current;\n    if (pos === null || !el) return;\n    pendingCaret.current = null;\n    if (document.activeElement === el) el.setSelectionRange(pos, pos);\n  });\n\n  const focusField = (el: HTMLInputElement | null) => {\n    if (!el) return;\n    el.focus();\n    const end = el.value.length;\n    el.setSelectionRange(end, end);\n  };\n\n  /* --------------------------------- handlers -------------------------------- */\n\n  const onNumberChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const el = event.currentTarget;\n    const raw = el.value;\n    const inputType = (event.nativeEvent as InputEvent).inputType ?? \"\";\n    const caret = el.selectionStart ?? raw.length;\n    let digits = raw.replace(/\\D/g, \"\");\n    let before = raw.slice(0, caret).replace(/\\D/g, \"\").length;\n    // A separator was deleted: remove the neighbouring digit instead, as the user intended.\n    if (digits === number && raw.length < formattedNumber.length) {\n      if (inputType === \"deleteContentForward\") digits = digits.slice(0, before) + digits.slice(before + 1);\n      else if (before > 0) {\n        digits = digits.slice(0, before - 1) + digits.slice(before);\n        before -= 1;\n      }\n    }\n    const nextBrand = detectCardBrand(digits);\n    digits = digits.slice(0, maxLength(nextBrand));\n    before = Math.min(before, digits.length);\n    const nextCvc = cvc.slice(0, ruleFor(nextBrand).cvc);\n    setNumber(digits);\n    if (nextCvc !== cvc) setCvc(nextCvc);\n    pendingCaret.current = caretAfter(before, nextBrand);\n    emit({ number: digits, cvc: nextCvc });\n\n    const nextRule = ruleFor(nextBrand);\n    const full = digits.length === maxLength(nextBrand) || (digits.length === 16 && nextRule.lengths.includes(16));\n    if (autoAdvance && inputType.startsWith(\"insert\") && before === digits.length && full && nextRule.lengths.includes(digits.length) && luhnCheck(digits)) {\n      focusField(expiryRef.current);\n    }\n  };\n\n  const onExpiryChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const raw = event.currentTarget.value;\n    const inputType = (event.nativeEvent as InputEvent).inputType ?? \"\";\n    const deleting = inputType.startsWith(\"delete\");\n    let digits: string;\n    // Autofill may supply MM/YYYY.\n    const full = raw.match(/^\\s*(\\d{1,2})\\s*\\/\\s*(\\d{4})\\s*$/);\n    if (full) digits = full[1].padStart(2, \"0\") + full[2].slice(2);\n    else {\n      digits = raw.replace(/\\D/g, \"\");\n      if (deleting && digits === expDigits) digits = digits.slice(0, -1); // deleted the \" / \"\n      if (digits.length === 1 && digits > \"1\") digits = `0${digits}`; // \"4\" → \"04\"\n      digits = digits.slice(0, 4);\n    }\n    const text = formatExpiry(digits, !deleting);\n    setExpiry(text);\n    emit({ expiry: text });\n    const month = Number(digits.slice(0, 2));\n    if (autoAdvance && !deleting && digits.length === 4 && month >= 1 && month <= 12) focusField(cvcRef.current);\n  };\n\n  const onCvcChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const digits = event.currentTarget.value.replace(/\\D/g, \"\").slice(0, rule.cvc);\n    const inputType = (event.nativeEvent as InputEvent).inputType ?? \"\";\n    setCvc(digits);\n    emit({ cvc: digits });\n    if (autoAdvance && inputType.startsWith(\"insert\") && digits.length === rule.cvc) focusField(nameRef.current);\n  };\n\n  const onNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const value = event.currentTarget.value.slice(0, 40);\n    setName(value);\n    emit({ name: value });\n  };\n\n  /** Backspace in an empty field steps back to the previous one. */\n  const stepBack = (event: React.KeyboardEvent<HTMLInputElement>, prev: \"number\" | \"expiry\" | \"cvc\") => {\n    if (event.key !== \"Backspace\" || event.currentTarget.value !== \"\") return;\n    const target = prev === \"number\" ? numberRef.current : prev === \"expiry\" ? expiryRef.current : cvcRef.current;\n    if (!target) return;\n    event.preventDefault();\n    focusField(target);\n  };\n\n  const focusProps = (field: CardField) => ({\n    onFocus: () => {\n      setFocused(field);\n      if (!today) {\n        const d = new Date();\n        setToday({ year: d.getFullYear(), month: d.getMonth() + 1 });\n      }\n    },\n    onBlur: () => {\n      setFocused((f) => (f === field ? null : f));\n      setTouched((t) => (t[field] ? t : { ...t, [field]: true }));\n    },\n    \"aria-invalid\": visible[field] ? true : undefined,\n    \"aria-describedby\": [field === \"number\" ? id(\"number-hint\") : null, visible[field] ? id(`${field}-error`) : null].filter(Boolean).join(\" \") || undefined,\n    disabled,\n  });\n\n  /* ---------------------------------- render --------------------------------- */\n\n  const inputClass = (field: CardField) =>\n    `h-11 w-full min-w-0 rounded-xl border bg-white px-3.5 text-[15px] tabular-nums text-zinc-950 caret-[var(--ci-accent)] outline-hidden transition-[border-color,box-shadow] duration-150 placeholder:text-zinc-400 disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none dark:bg-zinc-950 dark:text-zinc-50 dark:placeholder:text-zinc-600 ${\n      visible[field]\n        ? \"border-rose-500/80 focus:shadow-[0_0_0_3px_rgba(244,63,94,0.18)] dark:border-rose-400/80\"\n        : \"border-zinc-900/15 hover:border-zinc-900/30 focus:border-[var(--ci-accent)] focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ci-accent)_22%,transparent)] dark:border-white/15 dark:hover:border-white/30\"\n    }`;\n\n  const labelClass = \"mb-1.5 block font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\";\n\n  const errorText = (field: CardField) =>\n    visible[field] ? (\n      <p id={id(`${field}-error`)} className=\"mt-1.5 text-[12px] font-medium leading-snug text-rose-600 dark:text-rose-400\">\n        {visible[field]}\n      </p>\n    ) : null;\n\n  const flipped = flipOnCvc && focused === \"cvc\" && brand !== \"amex\";\n\n  // Preview number: typed digits, then placeholder dots up to the brand's usual length.\n  const templateLength = Math.max(brand === \"amex\" ? 15 : 16, number.length);\n  let previewNumber = \"\";\n  for (let i = 0; i < templateLength; i++) {\n    if (i > 0 && rule.gaps.includes(i)) previewNumber += \" \";\n    previewNumber += number[i] ?? \"•\";\n  }\n  const previewExpiry = `${expDigits.slice(0, 2).padEnd(2, \"•\")}/${expDigits.slice(2).padEnd(2, \"•\")}`;\n\n  const finish =\n    cardStyle === \"accent\"\n      ? \"bg-[linear-gradient(135deg,var(--ci-accent),color-mix(in_srgb,var(--ci-accent)_55%,#000))] text-zinc-950 border-black/10\"\n      : cardStyle === \"paper\"\n        ? \"bg-[linear-gradient(135deg,#FBFAF7,#ECE9E1)] text-zinc-900 border-zinc-900/10\"\n        : \"bg-[radial-gradient(120%_90%_at_100%_0%,color-mix(in_srgb,var(--ci-accent)_30%,transparent),transparent_55%),linear-gradient(135deg,#27272a,#09090b)] text-white border-white/10\";\n  const muted = cardStyle === \"graphite\" ? \"text-white/55\" : \"text-current opacity-60\";\n  const spot = (field: CardField) =>\n    `rounded-md px-1.5 py-1 -mx-1.5 -my-1 transition-[box-shadow] duration-200 motion-reduce:transition-none ${\n      focused === field ? (cardStyle === \"graphite\" ? \"shadow-[inset_0_0_0_1px_rgba(255,255,255,0.45)]\" : \"shadow-[inset_0_0_0_1px_rgba(0,0,0,0.35)]\") : \"\"\n    }`;\n\n  const faceBase = `absolute inset-0 overflow-hidden rounded-2xl border shadow-[0_24px_48px_-28px_rgba(0,0,0,0.6)] ${finish} [backface-visibility:hidden] [-webkit-backface-visibility:hidden]`;\n  const flipTransition = reduceMotion ? { duration: 0 } : { type: \"spring\" as const, stiffness: 260, damping: 26, mass: 0.9 };\n\n  const preview = (\n    <div aria-hidden=\"true\" className=\"mx-auto w-full max-w-[320px] [perspective:1100px]\">\n      <motion.div\n        initial={false}\n        animate={{ rotateY: flipped && !reduceMotion ? 180 : 0 }}\n        transition={flipTransition}\n        className=\"relative aspect-[1.586] w-full [transform-style:preserve-3d]\"\n      >\n        {/* Front */}\n        <div className={`${faceBase} flex flex-col justify-between p-5 ${reduceMotion && flipped ? \"opacity-0\" : \"\"}`}>\n          <div className=\"flex items-start justify-between\">\n            <svg viewBox=\"0 0 40 30\" className=\"h-7 w-9\">\n              <rect x=\"0.5\" y=\"0.5\" width=\"39\" height=\"29\" rx=\"6\" fill=\"#D9B97A\" stroke=\"rgba(0,0,0,0.25)\" />\n              <path d=\"M0.5 10.5h12M0.5 19.5h12M27.5 10.5h12M27.5 19.5h12M12.5 0.5v29M27.5 0.5v29\" stroke=\"rgba(0,0,0,0.28)\" fill=\"none\" />\n            </svg>\n            <div className=\"flex h-7 items-center text-[15px]\">\n              <BrandMark brand={brand} />\n              {!brand && <span className={`font-mono text-[10px] uppercase tracking-[0.14em] ${muted}`}>Card</span>}\n            </div>\n          </div>\n          <div className=\"flex items-end justify-between gap-2\">\n            <span className={`font-mono text-[clamp(13px,5.4cqw,19px)] tracking-[0.06em] tabular-nums ${spot(\"number\")}`}>{previewNumber}</span>\n            {brand === \"amex\" && (\n              <span className={`font-mono text-[11px] tabular-nums ${spot(\"cvc\")}`}>{cvc.padEnd(4, \"•\")}</span>\n            )}\n          </div>\n          <div className=\"flex items-end justify-between gap-4\">\n            <div className={`min-w-0 ${spot(\"name\")}`}>\n              <div className={`font-mono text-[8px] uppercase tracking-[0.16em] ${muted}`}>Card holder</div>\n              <div className=\"truncate text-[13px] font-medium uppercase tracking-[0.06em]\">{name.trim() || \"Your name\"}</div>\n            </div>\n            <div className={`shrink-0 text-right ${spot(\"expiry\")}`}>\n              <div className={`font-mono text-[8px] uppercase tracking-[0.16em] ${muted}`}>Expires</div>\n              <div className=\"font-mono text-[13px] tabular-nums\">{previewExpiry}</div>\n            </div>\n          </div>\n        </div>\n        {/* Back */}\n        <div\n          className={`${faceBase} ${reduceMotion ? (flipped ? \"\" : \"opacity-0\") : \"[transform:rotateY(180deg)]\"}`}\n        >\n          <div className=\"mt-6 h-10 w-full bg-black/80\" />\n          <div className=\"mx-5 mt-5 flex items-center gap-3\">\n            <div className=\"h-8 flex-1 rounded-sm bg-[repeating-linear-gradient(-45deg,rgba(255,255,255,0.9)_0_6px,rgba(228,228,231,0.9)_6px_12px)]\" />\n            <div className={`rounded-md bg-white px-2.5 py-1.5 font-mono text-[13px] tabular-nums text-zinc-900 ${focused === \"cvc\" ? \"shadow-[0_0_0_2px_var(--ci-accent)]\" : \"\"}`}>\n              {cvc.padEnd(rule.cvc, \"•\")}\n            </div>\n          </div>\n          <div className={`mx-5 mt-3 font-mono text-[8px] uppercase tracking-[0.16em] ${muted}`}>Security code</div>\n        </div>\n      </motion.div>\n    </div>\n  );\n\n  return (\n    <div\n      {...props}\n      className={`@container w-full ${className}`}\n      style={{ [\"--ci-accent\" as string]: accent, ...style }}\n    >\n      <div className={`grid grid-cols-1 gap-6 ${showPreview ? \"@[640px]:grid-cols-[minmax(0,300px)_minmax(0,1fr)] @[640px]:items-center @[640px]:gap-8\" : \"\"}`}>\n        {showPreview && <div className=\"@container\">{preview}</div>}\n\n        <fieldset className=\"min-w-0 space-y-4\" disabled={disabled}>\n          <legend className=\"sr-only\">Card details</legend>\n\n          <div>\n            <label htmlFor={id(\"number\")} className={labelClass}>\n              Card number\n            </label>\n            <div className=\"relative\">\n              <input\n                ref={numberRef}\n                id={id(\"number\")}\n                type=\"text\"\n                inputMode=\"numeric\"\n                autoComplete=\"cc-number\"\n                autoCorrect=\"off\"\n                spellCheck={false}\n                placeholder={numberPlaceholder}\n                value={formattedNumber}\n                onChange={onNumberChange}\n                {...focusProps(\"number\")}\n                className={`${inputClass(\"number\")} pr-24 font-mono tracking-[0.04em]`}\n              />\n              <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-y-0 right-3.5 flex items-center text-[13px] text-zinc-700 dark:text-zinc-300\">\n                {brand ? (\n                  <BrandMark brand={brand} />\n                ) : (\n                  <svg viewBox=\"0 0 24 16\" className=\"h-4 w-6 text-zinc-400 dark:text-zinc-600\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.3\">\n                    <rect x=\"0.65\" y=\"0.65\" width=\"22.7\" height=\"14.7\" rx=\"2.5\" />\n                    <path d=\"M0.65 5h22.7\" />\n                  </svg>\n                )}\n              </span>\n            </div>\n            <p id={id(\"number-hint\")} className=\"sr-only\" aria-live=\"polite\">\n              {brand ? `${BRANDS[brand].label} detected.` : \"\"}\n            </p>\n            {errorText(\"number\")}\n          </div>\n\n          <div className=\"grid grid-cols-2 gap-3\">\n            <div className=\"min-w-0\">\n              <label htmlFor={id(\"expiry\")} className={labelClass}>\n                Expiry\n              </label>\n              <input\n                ref={expiryRef}\n                id={id(\"expiry\")}\n                type=\"text\"\n                inputMode=\"numeric\"\n                autoComplete=\"cc-exp\"\n                autoCorrect=\"off\"\n                spellCheck={false}\n                placeholder=\"MM / YY\"\n                maxLength={7}\n                value={expiry}\n                onChange={onExpiryChange}\n                onKeyDown={(event) => stepBack(event, \"number\")}\n                {...focusProps(\"expiry\")}\n                className={`${inputClass(\"expiry\")} font-mono`}\n              />\n              {errorText(\"expiry\")}\n            </div>\n            <div className=\"min-w-0\">\n              <label htmlFor={id(\"cvc\")} className={labelClass}>\n                CVC\n              </label>\n              <input\n                ref={cvcRef}\n                id={id(\"cvc\")}\n                type=\"text\"\n                inputMode=\"numeric\"\n                autoComplete=\"cc-csc\"\n                autoCorrect=\"off\"\n                spellCheck={false}\n                placeholder={rule.cvc === 4 ? \"4 digits\" : \"3 digits\"}\n                maxLength={rule.cvc}\n                value={cvc}\n                onChange={onCvcChange}\n                onKeyDown={(event) => stepBack(event, \"expiry\")}\n                {...focusProps(\"cvc\")}\n                className={`${inputClass(\"cvc\")} font-mono`}\n              />\n              {errorText(\"cvc\")}\n            </div>\n          </div>\n\n          <div>\n            <label htmlFor={id(\"name\")} className={labelClass}>\n              Name on card{requireName ? \"\" : \" (optional)\"}\n            </label>\n            <input\n              ref={nameRef}\n              id={id(\"name\")}\n              type=\"text\"\n              autoComplete=\"cc-name\"\n              autoCapitalize=\"words\"\n              spellCheck={false}\n              placeholder=\"Ada Lovelace\"\n              value={name}\n              onChange={onNameChange}\n              onKeyDown={(event) => stepBack(event, \"cvc\")}\n              {...focusProps(\"name\")}\n              className={inputClass(\"name\")}\n            />\n            {errorText(\"name\")}\n          </div>\n        </fieldset>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "docs": "Documentation: https://wancraft.my/components/card-input"
}
