{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "bank-account-connector",
  "title": "Bank Account Connector",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/bank-account-connector/bank-account-connector.component.ts",
      "content": "// Plaid/Tink style bank linking flow: institution search, a popular-banks grid\n// (Chase, Bank of America, Wells Fargo, Citibank, Capital One, SVB), account radio\n// selection with balances and badges, and an AES-256 security footer. Port of the\n// Vue/React block 1:1 — emits `connect` with the linked account payload,\n// `cancel` when the flow is abandoned, and `selectBank` on institution pick.\nimport { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed, signal } from '@angular/core'\nimport {\n  ArrowLeft,\n  Building2,\n  CheckCircle2,\n  ExternalLink,\n  Loader2,\n  Lock,\n  LucideAngularModule,\n  Search,\n  Shield,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\n\nexport interface BankInstitution {\n  id: string\n  name: string\n  fullName: string\n  tagline: string\n}\n\nexport interface BankAccount {\n  id: string\n  name: string\n  last4: string\n  type: 'Checking' | 'Savings' | 'Credit'\n  balance: string\n  balanceLabel: string\n  subtitle: string\n}\n\nexport interface BankConnectPayload {\n  bankId: string\n  bankName: string\n  accountId: string\n  accountName: string\n  last4: string\n  balance: string\n}\n\nconst institutions: BankInstitution[] = [\n  { id: 'chase', name: 'Chase', fullName: 'JPMorgan Chase Bank', tagline: 'Checking, Savings & Credit' },\n  { id: 'bofa', name: 'Bank of America', fullName: 'Bank of America N.A.', tagline: 'Advantage Banking' },\n  { id: 'wellsfargo', name: 'Wells Fargo', fullName: 'Wells Fargo Bank', tagline: 'Everyday & Way2Save' },\n  { id: 'citibank', name: 'Citibank', fullName: 'Citibank N.A.', tagline: 'Citi Priority & Access' },\n  { id: 'capitalone', name: 'Capital One', fullName: 'Capital One Bank', tagline: '360 Checking & Savings' },\n  { id: 'svb', name: 'SVB', fullName: 'Silicon Valley Bank', tagline: 'Commercial & Startup Banking' },\n]\n\nconst accounts: BankAccount[] = [\n  {\n    id: 'acc-chk-4892',\n    name: 'Checking',\n    last4: '4892',\n    type: 'Checking',\n    balance: '$14,250.00',\n    balanceLabel: 'Available balance',\n    subtitle: 'Primary checking account',\n  },\n  {\n    id: 'acc-sav-9210',\n    name: 'Savings',\n    last4: '9210',\n    type: 'Savings',\n    balance: '$48,900.00',\n    balanceLabel: 'Available balance',\n    subtitle: 'High yield savings',\n  },\n  {\n    id: 'acc-crd-1049',\n    name: 'Business Credit',\n    last4: '1049',\n    type: 'Credit',\n    balance: '$5,200.00',\n    balanceLabel: 'Current balance',\n    subtitle: 'Corporate credit line',\n  },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-bank-icon',\n  standalone: true,\n  host: { '[attr.class]': '\"contents\"' },\n  template: `\n    @if (bankId === 'chase') {\n      <svg viewBox=\"0 0 24 24\" class=\"size-5 shrink-0 text-[#117ACA]\" fill=\"currentColor\" aria-hidden=\"true\">\n        <path\n          d=\"M12 2.5L7.5 7h4.8L16.2 3.1 12 2.5zm9.5 9.5L17 7.5v4.8l3.9 3.9.6-4.2zm-9.5 9.5L16.5 17h-4.8L7.8 20.9l4.2.6zm-9.5-9.5L7 16.5v-4.8L3.1 7.8 2.5 12z\"\n        />\n      </svg>\n    } @else if (bankId === 'bofa') {\n      <svg viewBox=\"0 0 24 24\" class=\"size-5 shrink-0\" fill=\"none\" aria-hidden=\"true\">\n        <rect x=\"2\" y=\"5\" width=\"8\" height=\"4\" rx=\"1\" fill=\"#E31837\" />\n        <rect x=\"2\" y=\"10.5\" width=\"8\" height=\"4\" rx=\"1\" fill=\"#002D62\" />\n        <rect x=\"2\" y=\"16\" width=\"8\" height=\"3\" rx=\"0.5\" fill=\"#E31837\" />\n        <rect x=\"14\" y=\"5\" width=\"8\" height=\"4\" rx=\"1\" fill=\"#002D62\" />\n        <rect x=\"14\" y=\"10.5\" width=\"8\" height=\"4\" rx=\"1\" fill=\"#E31837\" />\n        <rect x=\"14\" y=\"16\" width=\"8\" height=\"3\" rx=\"0.5\" fill=\"#002D62\" />\n      </svg>\n    } @else if (bankId === 'wellsfargo') {\n      <svg viewBox=\"0 0 24 24\" class=\"size-5 shrink-0\" fill=\"none\" aria-hidden=\"true\">\n        <rect width=\"24\" height=\"24\" rx=\"4\" fill=\"#D71E28\" />\n        <path d=\"M4 7h2.2l1.6 7 1.8-7h2.1l1.8 7 1.6-7H17.5l-2.6 10h-2.2l-1.8-7-1.8 7H7.1L4 7z\" fill=\"#FFCC00\" />\n      </svg>\n    } @else if (bankId === 'citibank') {\n      <svg viewBox=\"0 0 24 24\" class=\"size-5 shrink-0\" fill=\"none\" aria-hidden=\"true\">\n        <path d=\"M12 4.5C8 4.5 4.5 6.8 4 9.5h2.5c.5-1.5 3-2.5 5.5-2.5s5 1 5.5 2.5H20c-.5-2.7-4-5-8-5z\" fill=\"#EC111A\" />\n        <rect x=\"4\" y=\"11\" width=\"3\" height=\"8\" rx=\"0.5\" fill=\"#003B70\" />\n        <circle cx=\"12\" cy=\"15\" r=\"4\" fill=\"#003B70\" />\n        <rect x=\"17\" y=\"11\" width=\"3\" height=\"8\" rx=\"0.5\" fill=\"#003B70\" />\n      </svg>\n    } @else if (bankId === 'capitalone') {\n      <svg viewBox=\"0 0 24 24\" class=\"size-5 shrink-0\" fill=\"none\" aria-hidden=\"true\">\n        <rect width=\"24\" height=\"24\" rx=\"4\" fill=\"#004879\" />\n        <path d=\"M3.5 15.5C8 9.5 16 8 20.5 12c-5-2-12-1-15 3.5z\" fill=\"#D03027\" />\n        <rect x=\"7\" y=\"9\" width=\"10\" height=\"2\" rx=\"1\" fill=\"#FFFFFF\" />\n      </svg>\n    } @else if (bankId === 'svb') {\n      <svg viewBox=\"0 0 24 24\" class=\"size-5 shrink-0\" fill=\"none\" aria-hidden=\"true\">\n        <rect width=\"24\" height=\"24\" rx=\"4\" fill=\"#005587\" />\n        <path d=\"M6 15l6-9 6 9-3 1.5-3-4.5-3 4.5L6 15z\" fill=\"#FFFFFF\" />\n        <path d=\"M9 16.5l3-4.5 3 4.5-3 1.5-3-1.5z\" fill=\"#00A3E0\" />\n      </svg>\n    } @else {\n      <lucide-icon [img]=\"Building2\" class=\"text-muted-foreground size-5\" />\n    }\n  `,\n  imports: [LucideAngularModule],\n})\nclass UiBankIconComponent {\n  @Input() bankId = ''\n  protected readonly Building2 = Building2\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-bank-account-connector, [ui-bank-account-connector]',\n  standalone: true,\n  // The block renders its own root card: keep the host out of layout.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiBankIconComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiInputComponent,\n    UiSeparatorComponent,\n  ],\n  template: `\n    <div ui-card data-slot=\"bank-account-connector\" [class]=\"rootClass\">\n      <div ui-card-content class=\"space-y-5 p-6\">\n        <!-- Step 1: Institution Selection -->\n        @if (!selectedBankId()) {\n          <div class=\"space-y-5\">\n            <!-- Header -->\n            <div class=\"flex items-start gap-3.5\">\n              <div\n                class=\"bg-success/10 text-success ring-success/5 dark:bg-success/10 text-success flex size-10 shrink-0 items-center justify-center rounded-full ring-4\"\n              >\n                <lucide-icon [img]=\"Shield\" class=\"size-5\" />\n              </div>\n              <div class=\"min-w-0 space-y-1\">\n                <h3 class=\"text-foreground text-base leading-tight font-semibold tracking-tight\">\n                  Connect your bank account\n                </h3>\n                <p class=\"text-muted-foreground text-xs\">\n                  Secure 256-bit encrypted connection powered by open banking.\n                </p>\n              </div>\n            </div>\n\n            <!-- Search input -->\n            <div class=\"relative\">\n              <lucide-icon\n                [img]=\"Search\"\n                class=\"text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2\"\n              />\n              <ui-input\n                [value]=\"searchQuery()\"\n                (valueChange)=\"searchQuery.set($event)\"\n                placeholder=\"Search institutions (e.g. Chase, Wells Fargo)...\"\n                class=\"pl-9 text-sm\"\n              />\n            </div>\n\n            <!-- Popular Institutions Grid -->\n            <div class=\"space-y-2.5\">\n              <div class=\"flex items-center justify-between\">\n                <p class=\"text-muted-foreground text-xs font-semibold tracking-wider uppercase\">Popular institutions</p>\n                <span class=\"text-muted-foreground text-xs\">10,000+ supported</span>\n              </div>\n\n              @if (filteredInstitutions().length > 0) {\n                <div class=\"grid grid-cols-2 gap-2.5 sm:grid-cols-3\">\n                  @for (bank of filteredInstitutions(); track bank.id) {\n                    <button\n                      type=\"button\"\n                      class=\"group border-border bg-card hover:border-primary/40 hover:bg-muted/40 focus-visible:ring-ring flex flex-col items-center justify-center rounded-xl border p-3.5 text-center transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\"\n                      (click)=\"handleSelectBank(bank.id)\"\n                    >\n                      <div\n                        class=\"border-border/60 bg-muted/60 dark:bg-muted/30 mb-2 flex size-10 items-center justify-center rounded-lg border shadow-xs transition-transform group-hover:scale-105\"\n                      >\n                        <ui-bank-icon [bankId]=\"bank.id\" />\n                      </div>\n\n                      <span class=\"text-foreground w-full truncate text-xs font-semibold\">{{ bank.name }}</span>\n                      <span class=\"text-muted-foreground mt-0.5 w-full truncate text-xs\">{{ bank.tagline }}</span>\n                    </button>\n                  }\n                </div>\n              } @else {\n                <!-- Direct search fallback for non-popular queries -->\n                <div class=\"border-border rounded-xl border border-dashed p-4 text-center\">\n                  <p class=\"text-muted-foreground mb-2 text-xs\">\n                    No popular bank matched &ldquo;{{ searchQuery() }}&rdquo;. Connect via direct institution link:\n                  </p>\n                  <button ui-button size=\"sm\" variant=\"outline\" class=\"gap-1.5 text-xs\" (click)=\"connectDirect()\">\n                    <lucide-icon [img]=\"Building2\" class=\"text-muted-foreground size-3.5\" />\n                    Connect with {{ searchQuery() }}\n                  </button>\n                </div>\n              }\n            </div>\n          </div>\n        } @else {\n          <!-- Step 2: Account Selection Step -->\n          <div class=\"space-y-5\">\n            <!-- Back and Banner Header -->\n            <div class=\"space-y-3\">\n              <div class=\"flex items-center justify-between\">\n                <button\n                  ui-button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  class=\"text-muted-foreground hover:text-foreground -ml-2 h-7 gap-1 px-2 text-xs\"\n                  (click)=\"selectedBankId.set(null)\"\n                >\n                  <lucide-icon [img]=\"ArrowLeft\" class=\"size-3.5\" />\n                  Back to banks\n                </button>\n                <span class=\"text-muted-foreground font-mono text-xs\">Step 2 of 2</span>\n              </div>\n\n              <!-- Bank Banner -->\n              <div class=\"border-border bg-muted/40 flex items-center justify-between rounded-xl border p-3.5\">\n                <div class=\"flex min-w-0 items-center gap-3\">\n                  <div\n                    class=\"border-border/80 bg-background flex size-10 shrink-0 items-center justify-center rounded-lg border shadow-xs\"\n                  >\n                    <ui-bank-icon [bankId]=\"activeBank().id\" />\n                  </div>\n\n                  <div class=\"min-w-0\">\n                    <p class=\"text-foreground truncate text-sm font-semibold\">\n                      {{ activeBank().name }} Checking &amp; Savings\n                    </p>\n                    <div class=\"mt-0.5 flex items-center gap-1.5\">\n                      <span class=\"bg-success size-1.5 animate-pulse rounded-full\"></span>\n                      <span class=\"text-success text-xs font-medium\">Connected</span>\n                    </div>\n                  </div>\n                </div>\n\n                <button\n                  ui-button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  class=\"text-muted-foreground hover:text-foreground h-7 shrink-0 text-xs\"\n                  (click)=\"selectedBankId.set(null)\"\n                >\n                  Change\n                </button>\n              </div>\n            </div>\n\n            <!-- Account Radio Cards -->\n            <div class=\"space-y-2.5\">\n              <div class=\"space-y-1\">\n                <h4 class=\"text-foreground text-sm font-semibold\">Select an account</h4>\n                <p class=\"text-muted-foreground text-xs\">\n                  Choose the primary account to link for transactions and balance tracking.\n                </p>\n              </div>\n\n              <div role=\"radiogroup\" aria-label=\"Bank accounts\" class=\"space-y-2\">\n                @for (account of bankAccounts; track account.id) {\n                  <div\n                    role=\"radio\"\n                    [attr.aria-checked]=\"selectedAccountId() === account.id\"\n                    tabindex=\"0\"\n                    [class]=\"accountCardClass(account.id)\"\n                    (click)=\"selectedAccountId.set(account.id)\"\n                    (keydown.enter)=\"selectedAccountId.set(account.id)\"\n                    (keydown.space)=\"selectedAccountId.set(account.id); $event.preventDefault()\"\n                  >\n                    <div class=\"flex min-w-0 items-center gap-3\">\n                      <!-- Custom Radio Indicator -->\n                      <div [class]=\"radioClass(account.id)\">\n                        @if (selectedAccountId() === account.id) {\n                          <div class=\"bg-primary-foreground size-1.5 rounded-full\"></div>\n                        }\n                      </div>\n\n                      <!-- Account Info -->\n                      <div class=\"min-w-0 space-y-0.5\">\n                        <div class=\"flex flex-wrap items-center gap-2\">\n                          <span class=\"text-foreground truncate text-sm font-medium\">{{ account.name }}</span>\n                          <span class=\"text-muted-foreground font-mono text-xs\">•••• {{ account.last4 }}</span>\n                          <span\n                            ui-badge\n                            [variant]=\"account.type === 'Credit' ? 'outline' : 'secondary'\"\n                            class=\"h-5 px-2 py-0 text-xs font-normal\"\n                          >\n                            {{ account.type }}\n                          </span>\n                        </div>\n                        <p class=\"text-muted-foreground text-xs\">{{ account.subtitle }}</p>\n                      </div>\n                    </div>\n\n                    <!-- Balance -->\n                    <div class=\"shrink-0 pl-3 text-right\">\n                      <div class=\"text-foreground font-mono text-sm font-semibold tabular-nums\">\n                        {{ account.balance }}\n                      </div>\n                      <div class=\"text-muted-foreground text-xs\">{{ account.balanceLabel }}</div>\n                    </div>\n                  </div>\n                }\n              </div>\n            </div>\n          </div>\n        }\n\n        <!-- Security Footer -->\n        <div class=\"space-y-4 pt-2\">\n          <ui-separator />\n\n          <div class=\"flex flex-wrap items-center justify-between gap-2\">\n            <span\n              ui-badge\n              variant=\"outline\"\n              class=\"text-muted-foreground border-border/80 gap-1.5 py-1 text-xs font-normal\"\n            >\n              <lucide-icon [img]=\"Lock\" class=\"text-success size-3 shrink-0\" />\n              <span>Encrypted with AES-256</span>\n            </span>\n\n            <a\n              href=\"#\"\n              class=\"text-muted-foreground hover:text-foreground flex min-h-6 items-center gap-1 text-xs underline underline-offset-4 transition-colors\"\n              (click)=\"$event.preventDefault()\"\n            >\n              Privacy policy\n              <lucide-icon [img]=\"ExternalLink\" class=\"size-3\" />\n            </a>\n          </div>\n\n          <div class=\"flex flex-col-reverse gap-2 pt-1 sm:flex-row sm:items-center sm:justify-end\">\n            <button\n              ui-button\n              variant=\"ghost\"\n              size=\"sm\"\n              class=\"text-muted-foreground hover:text-foreground h-9 text-xs\"\n              (click)=\"handleCancel()\"\n            >\n              Cancel\n            </button>\n            <button\n              ui-button\n              size=\"sm\"\n              class=\"h-9 text-xs font-medium\"\n              [disabled]=\"isConnecting() || isConnected()\"\n              (click)=\"handleConnect()\"\n            >\n              @if (isConnecting()) {\n                <lucide-icon [img]=\"Loader2\" class=\"mr-1.5 size-3.5 animate-spin\" />\n                <span>Connecting…</span>\n              } @else if (isConnected()) {\n                <lucide-icon [img]=\"CheckCircle2\" class=\"text-success mr-1.5 size-3.5\" />\n                <span>Account Connected!</span>\n              } @else if (!selectedBankId()) {\n                <span>Continue</span>\n              } @else {\n                <span>Connect Selected Account</span>\n              }\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiBankAccountConnectorComponent {\n  protected readonly ArrowLeft = ArrowLeft\n  protected readonly Building2 = Building2\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly ExternalLink = ExternalLink\n  protected readonly Loader2 = Loader2\n  protected readonly Lock = Lock\n  protected readonly Search = Search\n  protected readonly Shield = Shield\n  protected readonly bankAccounts = accounts\n\n  @Input() set initialBankId(v: string | undefined) {\n    this.initialBankIdValue = v ?? null\n    if (v) this.selectedBankId.set(v)\n  }\n  @Input('class') className?: string\n\n  @Output() readonly connect = new EventEmitter<BankConnectPayload>()\n  @Output() readonly cancel = new EventEmitter<void>()\n  @Output() readonly selectBank = new EventEmitter<string>()\n\n  private initialBankIdValue: string | null = null\n\n  readonly searchQuery = signal('')\n  readonly selectedBankId = signal<string | null>(null)\n  readonly selectedAccountId = signal('acc-chk-4892')\n  readonly isConnecting = signal(false)\n  readonly isConnected = signal(false)\n\n  readonly filteredInstitutions = computed(() => {\n    const query = this.searchQuery().trim().toLowerCase()\n    if (!query) return institutions\n    return institutions.filter((b) => b.name.toLowerCase().includes(query) || b.fullName.toLowerCase().includes(query))\n  })\n\n  readonly activeBank = computed<BankInstitution>(() => {\n    const id = this.selectedBankId()\n    if (!id) return institutions[0]!\n    return (\n      institutions.find((b) => b.id === id) ?? {\n        id,\n        name: this.searchQuery() ? this.searchQuery() : 'Custom Institution',\n        fullName: 'Financial Institution',\n        tagline: 'Direct Open Banking',\n      }\n    )\n  })\n\n  readonly selectedAccount = computed<BankAccount>(() => {\n    return accounts.find((a) => a.id === this.selectedAccountId()) ?? accounts[0]!\n  })\n\n  get rootClass(): string {\n    return cn('mx-auto w-full max-w-lg overflow-hidden border shadow-sm', this.className)\n  }\n\n  accountCardClass(accountId: string): string {\n    const isSelected = this.selectedAccountId() === accountId\n    return cn(\n      'group focus-visible:ring-ring relative flex cursor-pointer items-center justify-between rounded-xl border p-3.5 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',\n      isSelected\n        ? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1'\n        : 'border-border bg-card hover:border-border/80 hover:bg-muted/30',\n    )\n  }\n\n  radioClass(accountId: string): string {\n    const isSelected = this.selectedAccountId() === accountId\n    return cn(\n      'flex size-4 shrink-0 items-center justify-center rounded-full border transition-colors',\n      isSelected\n        ? 'border-primary bg-primary text-primary-foreground'\n        : 'border-muted-foreground/30 bg-background group-hover:border-muted-foreground/60',\n    )\n  }\n\n  handleSelectBank(bankId: string): void {\n    this.selectedBankId.set(bankId)\n    this.selectBank.emit(bankId)\n  }\n\n  connectDirect(): void {\n    this.handleSelectBank(this.searchQuery().toLowerCase().replace(/\\s+/g, '-'))\n  }\n\n  handleConnect(): void {\n    if (!this.selectedBankId()) {\n      this.selectedBankId.set(institutions[0]!.id)\n      return\n    }\n\n    this.isConnecting.set(true)\n    setTimeout(() => {\n      this.isConnecting.set(false)\n      this.isConnected.set(true)\n      const bank = this.activeBank()\n      const account = this.selectedAccount()\n      this.connect.emit({\n        bankId: bank.id,\n        bankName: bank.name,\n        accountId: account.id,\n        accountName: account.name,\n        last4: account.last4,\n        balance: account.balance,\n      })\n      setTimeout(() => {\n        this.isConnected.set(false)\n      }, 2500)\n    }, 900)\n  }\n\n  handleCancel(): void {\n    if (this.selectedBankId() && !this.initialBankIdValue) {\n      this.selectedBankId.set(null)\n    }\n    this.cancel.emit()\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/bank-account-connector/bank-account-connector.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/bank-account-connector/index.ts",
      "content": "export {\n  UiBankAccountConnectorComponent,\n  type BankInstitution,\n  type BankAccount,\n  type BankConnectPayload,\n} from './bank-account-connector.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/bank-account-connector/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/separator.json"
  ],
  "description": "Plaid/Tink style bank linking modal and account selector with institution search, popular bank grid (Chase, Bank of America, Wells Fargo, Citibank, Capital One, SVB), account radio selection with balances and badges, and an AES-256 encrypted security footer.",
  "categories": [
    "finance",
    "dashboard"
  ]
}