{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "saved-cards-list",
  "title": "Saved Cards List",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/saved-cards-list/SavedCardsList.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { CheckCircle2, Plus, Trash2 } from 'lucide-vue-next'\nimport { PaymentCard } from '@/components/ui/payment-card'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\nimport PaymentForm from '@/components/blocks/PaymentForm.vue'\n\ntype CardBrand = 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown'\n\ninterface SavedCard {\n  id: string\n  brand: CardBrand\n  last4: string\n  expiry: string\n  holder: string\n}\n\nconst props = defineProps<{\n  cards: SavedCard[]\n  defaultId?: string\n}>()\n\nconst emit = defineEmits<{\n  add: [\n    payload: {\n      number: string\n      name: string\n      expiry: string\n      cvc: string\n      brand: CardBrand\n    },\n  ]\n  remove: [id: string]\n  'set-default': [id: string]\n}>()\n\nconst showAdd = ref(false)\nconst removeTarget = ref<SavedCard | null>(null)\n\nfunction brandLabel(b: CardBrand) {\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\nconst confirmOpen = computed({\n  get: () => !!removeTarget.value,\n  set: (v) => {\n    if (!v) removeTarget.value = null\n  },\n})\n\nfunction confirmRemove() {\n  if (!removeTarget.value) return\n  emit('remove', removeTarget.value.id)\n  removeTarget.value = null\n}\n</script>\n\n<template>\n  <div class=\"bg-card text-card-foreground mx-auto w-full max-w-2xl rounded-xl border shadow-sm\">\n    <div class=\"flex items-center justify-between border-b px-5 py-4\">\n      <div>\n        <h3 class=\"text-base font-semibold\">Saved cards</h3>\n        <p class=\"text-muted-foreground text-xs\">Manage the cards used for billing.</p>\n      </div>\n      <Button size=\"sm\" @click=\"showAdd = !showAdd\"><Plus class=\"size-4\" /> Add card</Button>\n    </div>\n\n    <!-- Add form (collapses inline) -->\n    <Transition\n      enter-active-class=\"transition-all duration-300 ease-out\"\n      leave-active-class=\"transition-all duration-200 ease-in\"\n      enter-from-class=\"max-h-0 opacity-0\"\n      enter-to-class=\"max-h-[1000px] opacity-100\"\n      leave-from-class=\"max-h-[1000px] opacity-100\"\n      leave-to-class=\"max-h-0 opacity-0\"\n    >\n      <div v-if=\"showAdd\" class=\"bg-muted/30 overflow-hidden border-b p-4\">\n        <PaymentForm\n          :amount=\"0\"\n          :show-wallets=\"false\"\n          :on-submit=\"\n            async (p) => {\n              emit('add', p)\n              showAdd = false\n            }\n          \"\n        />\n      </div>\n    </Transition>\n\n    <!-- Empty state -->\n    <div v-if=\"cards.length === 0 && !showAdd\" 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        <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 size=\"sm\" class=\"mt-4\" @click=\"showAdd = true\">Add your first card</Button>\n    </div>\n\n    <!-- List -->\n    <ul v-else class=\"divide-border divide-y\">\n      <li\n        v-for=\"card in cards\"\n        :key=\"card.id\"\n        class=\"hover:bg-muted/30 flex items-center gap-4 px-5 py-4 transition-colors\"\n      >\n        <PaymentCard\n          :number=\"maskedNumberFromLast4(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\">{{ brandLabel(card.brand) }} ending {{ card.last4 }}</p>\n            <Badge v-if=\"card.id === defaultId\" variant=\"secondary\"><CheckCircle2 class=\"size-3\" /> Default</Badge>\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          <Button v-if=\"card.id !== defaultId\" variant=\"ghost\" size=\"sm\" @click=\"emit('set-default', card.id)\">\n            Set default\n          </Button>\n          <Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Remove card\" @click=\"removeTarget = card\">\n            <Trash2 class=\"text-muted-foreground size-4\" />\n          </Button>\n        </div>\n      </li>\n    </ul>\n\n    <!-- Remove confirmation -->\n    <Dialog v-model:open=\"confirmOpen\">\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Remove this card?</DialogTitle>\n          <DialogDescription>\n            {{ removeTarget && brandLabel(removeTarget.brand) }} ending {{ removeTarget?.last4 }} will no longer be\n            available for payments.\n          </DialogDescription>\n        </DialogHeader>\n        <DialogFooter>\n          <Button variant=\"outline\" @click=\"removeTarget = null\">Cancel</Button>\n          <Button variant=\"destructive\" @click=\"confirmRemove\">Remove</Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/SavedCardsList.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/payment-card.json",
    "https://uipkge.dev/r/vue/payment-form.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/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": [
    "commerce"
  ]
}