{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "payments",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Routing + account with checksum validation.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { CheckIcon } from \"@/components/ledger/icons\";\n\n/** ABA routing checksum: 3·(d1+d4+d7) + 7·(d2+d5+d8) + 1·(d3+d6+d9) ≡ 0 mod 10. */\nexport function validateAba(routing: string): boolean {\n  const d = routing.replace(/\\D/g, \"\");\n  if (d.length !== 9) return false;\n  const w = [3, 7, 1, 3, 7, 1, 3, 7, 1];\n  const sum = d.split(\"\").reduce((acc, ch, i) => acc + Number(ch) * w[i], 0);\n  return sum % 10 === 0;\n}\n\n/** IBAN mod-97: move the first four characters to the end, letters → numbers. */\nexport function validateIban(iban: string): boolean {\n  const s = iban.replace(/\\s/g, \"\").toUpperCase();\n  if (!/^[A-Z]{2}\\d{2}[A-Z0-9]{10,30}$/.test(s)) return false;\n  const rearranged = s.slice(4) + s.slice(0, 4);\n  const expanded = rearranged.replace(/[A-Z]/g, (c) => String(c.charCodeAt(0) - 55));\n  let remainder = 0;\n  for (const ch of expanded) remainder = (remainder * 10 + Number(ch)) % 97;\n  return remainder === 1;\n}\n\nconst INSTITUTIONS: Record<string, string> = {\n  \"021000021\": \"JPMorgan Chase Bank, N.A.\",\n  \"026009593\": \"Bank of America, N.A.\",\n  \"071000013\": \"JPMorgan Chase Bank, N.A. (Illinois)\",\n  \"121000248\": \"Wells Fargo Bank, N.A.\",\n};\n\nexport interface BankAccountInputProps {\n  className?: string;\n  mode?: \"us\" | \"iban\";\n  onChange?: (value: { routing: string; account: string; valid: boolean }) => void;\n}\n\n/**\n * The resolved institution name is the point: instant confirmation that the\n * user typed the right bank, before any money moves.\n */\nexport function BankAccountInput({ mode = \"us\", className, onChange }: BankAccountInputProps) {\n  const [routing, setRouting] = React.useState(\"\");\n  const [account, setAccount] = React.useState(\"\");\n  const [iban, setIban] = React.useState(\"\");\n\n  const routingValid = validateAba(routing);\n  const ibanValid = validateIban(iban);\n  const institution = routingValid\n    ? (INSTITUTIONS[routing.replace(/\\D/g, \"\")] ?? \"Verified routing number\")\n    : null;\n\n  React.useEffect(() => {\n    onChange?.(\n      mode === \"us\"\n        ? { account, routing, valid: routingValid && account.length >= 4 }\n        : { account: iban, routing: \"\", valid: ibanValid },\n    );\n  }, [mode, routing, account, iban, routingValid, ibanValid, onChange]);\n\n  if (mode === \"iban\") {\n    return (\n      <label className={cn(\"block\", className)}>\n        <span className=\"mb-2 block text-label font-medium text-text-secondary\">IBAN</span>\n        <input\n          aria-invalid={iban.length > 0 && !ibanValid}\n          aria-label=\"International bank account number\"\n          className={cn(\n            \"reference-numeric h-10 w-full rounded-sm border bg-surface px-3 text-body outline-none focus-visible:border-accent focus-visible:ring-2 focus-visible:ring-accent-subtle\",\n            iban && !ibanValid ? \"border-negative\" : \"border-border focus-visible:border-accent\",\n          )}\n          onChange={(e) => setIban(e.target.value.toUpperCase())}\n          placeholder=\"DE89 3704 0044 0532 0130 00\"\n          value={iban}\n        />\n        {iban && !ibanValid ? (\n          <span className=\"mt-1 block text-caption text-negative\" role=\"alert\">\n            That IBAN fails its checksum — check the country code and digits.\n          </span>\n        ) : ibanValid ? (\n          <span className=\"mt-1 flex items-center gap-1 text-caption text-positive\">\n            <CheckIcon aria-hidden=\"true\" className=\"size-3\" /> Valid IBAN\n          </span>\n        ) : null}\n      </label>\n    );\n  }\n\n  return (\n    <div className={cn(\"grid gap-3 sm:grid-cols-2\", className)}>\n      <label className=\"block\">\n        <span className=\"mb-2 block text-label font-medium text-text-secondary\">Routing number</span>\n        <input\n          aria-invalid={routing.length === 9 && !routingValid}\n          aria-label=\"ABA routing number\"\n          className={cn(\n            \"reference-numeric h-10 w-full rounded-sm border bg-surface px-3 text-body outline-none focus-visible:border-accent focus-visible:ring-2 focus-visible:ring-accent-subtle\",\n            routing.length === 9 && !routingValid\n              ? \"border-negative\"\n              : \"border-border focus-visible:border-accent\",\n          )}\n          inputMode=\"numeric\"\n          maxLength={9}\n          onChange={(e) => setRouting(e.target.value.replace(/\\D/g, \"\"))}\n          placeholder=\"021000021\"\n          value={routing}\n        />\n        {institution ? (\n          <span className=\"ledger-crossfade mt-1 flex items-center gap-1 text-caption text-positive\">\n            <CheckIcon aria-hidden=\"true\" className=\"size-3\" /> {institution}\n          </span>\n        ) : routing.length === 9 ? (\n          <span className=\"mt-1 block text-caption text-negative\" role=\"alert\">\n            That routing number fails its checksum.\n          </span>\n        ) : null}\n      </label>\n\n      <label className=\"block\">\n        <span className=\"mb-2 block text-label font-medium text-text-secondary\">Account number</span>\n        <input\n          aria-label=\"Bank account number\"\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) => setAccount(e.target.value.replace(/\\D/g, \"\"))}\n          value={account}\n        />\n      </label>\n    </div>\n  );\n}\n",
      "path": "components/ledger/bank-account-input.tsx",
      "target": "components/ledger/bank-account-input.tsx",
      "type": "registry:component"
    }
  ],
  "name": "bank-account-input",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/icons"
  ],
  "title": "BankAccountInput",
  "type": "registry:component"
}
