{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "payments",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "PAN / expiry / CVC with correct autocomplete.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\n\ntype Network = \"visa\" | \"mastercard\" | \"amex\" | \"discover\" | null;\n\n/** IIN detection — the mark appears inline as soon as it is knowable. */\nfunction detectNetwork(pan: string): Network {\n  const d = pan.replace(/\\D/g, \"\");\n  if (/^4/.test(d)) return \"visa\";\n  if (/^(5[1-5]|2[2-7])/.test(d)) return \"mastercard\";\n  if (/^3[47]/.test(d)) return \"amex\";\n  if (/^6(?:011|5)/.test(d)) return \"discover\";\n  return null;\n}\n\nfunction luhn(pan: string): boolean {\n  const d = pan.replace(/\\D/g, \"\");\n  if (d.length < 12) return false;\n  let sum = 0;\n  let double = false;\n  for (let i = d.length - 1; i >= 0; i--) {\n    let n = Number(d[i]);\n    if (double) {\n      n *= 2;\n      if (n > 9) n -= 9;\n    }\n    sum += n;\n    double = !double;\n  }\n  return sum % 10 === 0;\n}\n\nfunction groupPan(pan: string, network: Network): string {\n  const d = pan.replace(/\\D/g, \"\").slice(0, network === \"amex\" ? 15 : 19);\n  if (network === \"amex\") {\n    return [d.slice(0, 4), d.slice(4, 10), d.slice(10, 15)].filter(Boolean).join(\" \");\n  }\n  return (d.match(/.{1,4}/g) ?? []).join(\" \");\n}\n\nexport interface CardInputProps {\n  className?: string;\n  onChange?: (value: { pan: string; expiry: string; cvc: string; valid: boolean }) => void;\n}\n\n/**\n * Correct autocomplete tokens, Luhn validation, IIN detection, auto-advance —\n * and backspace returning to the previous field, the detail everyone forgets.\n */\nexport function CardInput({ className, onChange }: CardInputProps) {\n  const [pan, setPan] = React.useState(\"\");\n  const [expiry, setExpiry] = React.useState(\"\");\n  const [cvc, setCvc] = React.useState(\"\");\n  const panRef = React.useRef<HTMLInputElement>(null);\n  const expRef = React.useRef<HTMLInputElement>(null);\n  const cvcRef = React.useRef<HTMLInputElement>(null);\n\n  const network = detectNetwork(pan);\n  const cvcLength = network === \"amex\" ? 4 : 3;\n  const panDigits = pan.replace(/\\D/g, \"\");\n  const panValid = luhn(pan);\n\n  React.useEffect(() => {\n    onChange?.({\n      cvc,\n      expiry,\n      pan: panDigits,\n      valid: panValid && /^\\d{2}\\/\\d{2}$/.test(expiry) && cvc.length === cvcLength,\n    });\n  }, [panDigits, expiry, cvc, panValid, cvcLength, onChange]);\n\n  return (\n    <div className={cn(\"grid grid-cols-2 gap-3\", className)}>\n      <label className=\"col-span-2 block\">\n        <span className=\"mb-2 block text-label font-medium text-text-secondary\">Card number</span>\n        <span\n          className={cn(\n            \"flex items-center gap-2 rounded-sm border bg-surface px-3\",\n            pan && !panValid ? \"border-negative\" : \"border-border focus-within:border-accent\",\n          )}\n        >\n          <input\n            aria-invalid={pan.length > 0 && !panValid}\n            aria-label=\"Card number\"\n            autoComplete=\"cc-number\"\n            className=\"reference-numeric h-10 w-0 min-w-0 flex-1 bg-transparent text-body outline-none\"\n            inputMode=\"numeric\"\n            onChange={(e) => {\n              const next = e.target.value;\n              setPan(next);\n              const digits = next.replace(/\\D/g, \"\");\n              const full = detectNetwork(next) === \"amex\" ? 15 : 16;\n              if (digits.length >= full) expRef.current?.focus();\n            }}\n            placeholder=\"1234 5678 9012 3456\"\n            ref={panRef}\n            type=\"text\"\n            value={groupPan(pan, network)}\n          />\n          {network ? (\n            <span className=\"ledger-crossfade text-caption font-semibold uppercase text-text-secondary\">\n              {network}\n              <span className=\"sr-only\"> card</span>\n            </span>\n          ) : null}\n        </span>\n      </label>\n\n      <label className=\"block\">\n        <span className=\"mb-2 block text-label font-medium text-text-secondary\">Expiry</span>\n        <input\n          aria-label=\"Expiry date, month and year\"\n          autoComplete=\"cc-exp\"\n          className=\"reference-numeric h-10 w-full rounded-sm border border-border bg-surface px-3 text-body outline-none focus-visible:border-accent\"\n          inputMode=\"numeric\"\n          onChange={(e) => {\n            const d = e.target.value.replace(/\\D/g, \"\").slice(0, 4);\n            const next = d.length > 2 ? `${d.slice(0, 2)}/${d.slice(2)}` : d;\n            setExpiry(next);\n            if (d.length === 4) cvcRef.current?.focus();\n          }}\n          onKeyDown={(e) => {\n            if (e.key === \"Backspace\" && expiry === \"\") panRef.current?.focus();\n          }}\n          placeholder=\"MM/YY\"\n          ref={expRef}\n          type=\"text\"\n          value={expiry}\n        />\n      </label>\n\n      <label className=\"block\">\n        <span className=\"mb-2 block text-label font-medium text-text-secondary\">Security code</span>\n        <input\n          aria-label=\"Security code\"\n          autoComplete=\"cc-csc\"\n          className=\"reference-numeric h-10 w-full rounded-sm border border-border bg-surface px-3 text-body outline-none focus-visible:border-accent\"\n          inputMode=\"numeric\"\n          onChange={(e) => setCvc(e.target.value.replace(/\\D/g, \"\").slice(0, cvcLength))}\n          onKeyDown={(e) => {\n            if (e.key === \"Backspace\" && cvc === \"\") expRef.current?.focus();\n          }}\n          placeholder={\"•\".repeat(cvcLength)}\n          ref={cvcRef}\n          type=\"text\"\n          value={cvc}\n        />\n      </label>\n\n      {pan && !panValid ? (\n        <p className=\"col-span-2 text-caption text-negative\" role=\"alert\">\n          That card number doesn’t look right — check the digits.\n        </p>\n      ) : null}\n    </div>\n  );\n}\n",
      "path": "components/ledger/card-input.tsx",
      "target": "components/ledger/card-input.tsx",
      "type": "registry:component"
    }
  ],
  "name": "card-input",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn"
  ],
  "title": "CardInput",
  "type": "registry:component"
}
