{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "saved-cards-list",
  "title": "Saved Cards List",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/saved-cards-list/saved-cards-list.component.ts",
      "content": "// Saved cards list: stored payment cards with compact card visuals, a default badge,\n// set-default / remove-with-confirm actions, and an inline PaymentForm that collapses\n// open. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed, signal } from '@angular/core'\nimport { CheckCircle2, LucideAngularModule, Plus, Trash2 } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiDialogComponent,\n  UiDialogContentComponent,\n  UiDialogDescriptionComponent,\n  UiDialogFooterComponent,\n  UiDialogHeaderComponent,\n  UiDialogTitleComponent,\n} from '@/ui/dialog/dialog.component'\nimport { UiPaymentCardComponent } from '@/ui/payment-card/payment-card.component'\nimport {\n  UiPaymentFormComponent,\n  type PaymentFormCardBrand,\n  type PaymentPayload,\n} from '../payment-form/payment-form.component'\n\nexport type CardBrand = 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown'\n\nexport interface SavedCard {\n  id: string\n  brand: CardBrand\n  last4: string\n  expiry: string\n  holder: string\n}\n\nexport interface AddPayload {\n  number: string\n  name: string\n  expiry: string\n  cvc: string\n  brand: CardBrand\n}\n\nconst defaultCards: SavedCard[] = [\n  { id: 'c1', brand: 'visa', last4: '4242', expiry: '12/29', holder: 'Jane Doe' },\n  { id: 'c2', brand: 'mastercard', last4: '4444', expiry: '08/27', holder: 'Jane Doe' },\n  { id: 'c3', brand: 'amex', last4: '0005', expiry: '03/30', holder: 'Jane Doe' },\n]\n\nfunction brandLabel(b: CardBrand): string {\n  return { visa: 'Visa', mastercard: 'Mastercard', amex: 'Amex', discover: 'Discover', unknown: 'Card' }[b]\n}\n\nfunction maskedNumberFromLast4(last4: string, brand: CardBrand): string {\n  if (brand === 'amex') return `•••• •••••• •${last4}`\n  return `•••• •••• •••• ${last4}`\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-saved-cards-list, [ui-saved-cards-list]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiDialogComponent,\n    UiDialogContentComponent,\n    UiDialogDescriptionComponent,\n    UiDialogFooterComponent,\n    UiDialogHeaderComponent,\n    UiDialogTitleComponent,\n    UiPaymentCardComponent,\n    UiPaymentFormComponent,\n  ],\n  template: `\n    <div data-slot=\"saved-cards-list\" [class]=\"rootClass\">\n      <div class=\"flex items-center justify-between border-b px-5 py-4\">\n        <div>\n          <h3 class=\"text-base font-semibold\">{{ title() }}</h3>\n          <p class=\"text-muted-foreground text-xs\">{{ description() }}</p>\n        </div>\n        <button ui-button size=\"sm\" (click)=\"toggleAdd()\">\n          <lucide-icon [img]=\"Plus\" class=\"size-4\" />\n          <span>Add card</span>\n        </button>\n      </div>\n\n      <!-- Add form (collapses inline) -->\n      @if (showAdd()) {\n        <div class=\"bg-muted/30 max-h-[1000px] overflow-hidden border-b p-4 opacity-100\">\n          <ui-payment-form [amount]=\"0\" [showWallets]=\"false\" [onSubmit]=\"submitAdd\" />\n        </div>\n      }\n\n      <!-- Empty state -->\n      @if (!hasRows() && !showAdd()) {\n        <div class=\"px-6 py-14 text-center\">\n          <div class=\"bg-muted text-muted-foreground mx-auto grid size-12 place-items-center rounded-full\">\n            <lucide-icon [img]=\"Plus\" class=\"size-5\" />\n          </div>\n          <h4 class=\"mt-3 text-sm font-medium\">No cards saved</h4>\n          <p class=\"text-muted-foreground text-xs\">Add a card to use it for future payments.</p>\n          <button ui-button size=\"sm\" class=\"mt-4\" (click)=\"showAdd.set(true)\">Add your first card</button>\n        </div>\n      } @else {\n        <ul class=\"divide-border divide-y\">\n          @for (card of cards(); track card.id) {\n            <li class=\"hover:bg-muted/30 flex items-center gap-4 px-5 py-4 transition-colors\">\n              <ui-payment-card\n                [number]=\"masked(card.last4, card.brand)\"\n                [name]=\"card.holder\"\n                [expiry]=\"card.expiry\"\n                [brand]=\"card.brand\"\n                variant=\"compact\"\n                [flip]=\"false\"\n              />\n              <div class=\"min-w-0 flex-1\">\n                <div class=\"flex items-center gap-2\">\n                  <p class=\"text-sm font-medium\">{{ label(card.brand) }} ending {{ card.last4 }}</p>\n                  @if (card.id === defaultId()) {\n                    <span ui-badge variant=\"secondary\">\n                      <lucide-icon [img]=\"CheckCircle2\" class=\"size-3\" />\n                      <span>Default</span>\n                    </span>\n                  }\n                </div>\n                <p class=\"text-muted-foreground text-xs\">Expires {{ card.expiry }} · {{ card.holder }}</p>\n              </div>\n              <div class=\"flex gap-1\">\n                @if (card.id !== defaultId()) {\n                  <button ui-button variant=\"ghost\" size=\"sm\" (click)=\"setDefault(card.id)\">Set default</button>\n                }\n                <button\n                  ui-button\n                  variant=\"ghost\"\n                  size=\"icon-sm\"\n                  aria-label=\"Remove card\"\n                  (click)=\"confirmId.set(card.id)\"\n                >\n                  <lucide-icon [img]=\"Trash2\" class=\"text-muted-foreground size-4\" />\n                </button>\n              </div>\n            </li>\n          }\n        </ul>\n      }\n\n      <!-- Remove confirmation -->\n      <ui-dialog [open]=\"confirmCard() !== null\" (openChange)=\"onConfirmOpenChange($event)\">\n        @if (confirmCard(); as target) {\n          <ui-dialog-content>\n            <div ui-dialog-header>\n              <h2 ui-dialog-title>Remove this card?</h2>\n              <p ui-dialog-description>\n                {{ label(target.brand) }} ending {{ target.last4 }} will no longer be available for payments.\n              </p>\n            </div>\n            <div ui-dialog-footer>\n              <button ui-button variant=\"outline\" (click)=\"confirmId.set(null)\">Cancel</button>\n              <button ui-button variant=\"destructive\" (click)=\"confirmRemove()\">Remove</button>\n            </div>\n          </ui-dialog-content>\n        }\n      </ui-dialog>\n    </div>\n  `,\n})\nexport class UiSavedCardsListComponent {\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly Plus = Plus\n  protected readonly Trash2 = Trash2\n\n  @Input('title') set titleInput(v: string | undefined) {\n    this.title.set(v ?? 'Saved cards')\n  }\n  @Input('description') set descriptionInput(v: string | undefined) {\n    this.description.set(v ?? 'Manage the cards used for billing.')\n  }\n  @Input() set initialCards(v: SavedCard[] | undefined) {\n    this.cards.set((v ?? defaultCards).map((c) => ({ ...c })))\n    if (!this.defaultId()) this.defaultId.set((v ?? defaultCards)[0]?.id ?? '')\n  }\n  @Input() set initialDefaultId(v: string | undefined) {\n    if (v) this.defaultId.set(v)\n  }\n  @Input() set initialShowAdd(v: boolean) {\n    this.showAdd.set(!!v)\n  }\n  @Input('class') className?: string\n\n  @Output() readonly add = new EventEmitter<AddPayload>()\n  @Output() readonly remove = new EventEmitter<string>()\n  @Output() readonly setDefaultChange = new EventEmitter<string>()\n\n  readonly title = signal('Saved cards')\n  readonly description = signal('Manage the cards used for billing.')\n  readonly cards = signal<SavedCard[]>(defaultCards.map((c) => ({ ...c })))\n  readonly defaultId = signal<string>(defaultCards[0]?.id ?? '')\n  readonly showAdd = signal(false)\n  readonly confirmId = signal<string | null>(null)\n\n  readonly hasRows = computed(() => this.cards().length > 0)\n  readonly confirmCard = computed(() => this.cards().find((c) => c.id === this.confirmId()) ?? null)\n\n  get rootClass(): string {\n    return cn('bg-card text-card-foreground mx-auto w-full max-w-2xl rounded-xl border shadow-sm', this.className)\n  }\n\n  label(b: CardBrand): string {\n    return brandLabel(b)\n  }\n\n  masked(last4: string, brand: CardBrand): string {\n    return maskedNumberFromLast4(last4, brand)\n  }\n\n  toggleAdd(): void {\n    this.showAdd.update((v) => !v)\n  }\n\n  readonly submitAdd = async (p: PaymentPayload): Promise<void> => {\n    this.handleAdd({\n      number: p.number,\n      name: p.name,\n      expiry: p.expiry,\n      cvc: p.cvc,\n      brand: p.brand as CardBrand,\n    })\n  }\n\n  handleAdd(payload: AddPayload): void {\n    const card: SavedCard = {\n      id: `c${Date.now()}`,\n      brand: payload.brand as CardBrand,\n      last4: payload.number.replace(/\\D/g, '').slice(-4),\n      expiry: payload.expiry,\n      holder: payload.name,\n    }\n    this.cards.update((prev) => [...prev, card])\n    if (!this.defaultId()) this.defaultId.set(card.id)\n    this.showAdd.set(false)\n    this.add.emit(payload)\n  }\n\n  setDefault(id: string): void {\n    this.defaultId.set(id)\n    this.setDefaultChange.emit(id)\n  }\n\n  confirmRemove(): void {\n    const id = this.confirmId()\n    if (!id) return\n    this.cards.update((prev) => {\n      const next = prev.filter((c) => c.id !== id)\n      if (this.defaultId() === id) this.defaultId.set(next[0]?.id ?? '')\n      return next\n    })\n    this.confirmId.set(null)\n    this.remove.emit(id)\n  }\n\n  onConfirmOpenChange(open: boolean): void {\n    if (!open) this.confirmId.set(null)\n  }\n}\n\nexport type { PaymentFormCardBrand }\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/saved-cards-list/saved-cards-list.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/saved-cards-list/index.ts",
      "content": "export {\n  UiSavedCardsListComponent,\n  type CardBrand,\n  type SavedCard,\n  type AddPayload,\n} from './saved-cards-list.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/saved-cards-list/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/payment-card.json",
    "https://uipkge.dev/r/angular/payment-form.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/dialog.json"
  ],
  "description": "List of stored payment cards using compact 3D card visuals. Marks one as the default, allows setting a different one as default, removing with a confirmation dialog, and adding a new card via an inline PaymentForm that collapses open. Emits `add`, `remove`, and `set-default` — consumer owns the persistence.",
  "categories": [
    "finance",
    "commerce"
  ]
}