{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "saved-cards-list",
  "title": "Saved Cards List",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/saved-cards-list/SavedCardsList.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { CheckCircle2, Plus, Trash2 } from 'lucide-react'\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'\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\ninterface AddPayload {\n  number: string\n  name: string\n  expiry: string\n  cvc: string\n  brand: CardBrand\n}\n\nexport interface SavedCardsListProps {\n  cards: SavedCard[]\n  defaultId?: string\n  onAdd?: (payload: AddPayload) => void\n  onRemove?: (id: string) => void\n  onSetDefault?: (id: string) => void\n}\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\nexport function SavedCardsList({ cards, defaultId, onAdd, onRemove, onSetDefault }: SavedCardsListProps) {\n  const [showAdd, setShowAdd] = React.useState(false)\n  const [removeTarget, setRemoveTarget] = React.useState<SavedCard | null>(null)\n\n  const confirmOpen = !!removeTarget\n\n  function confirmRemove() {\n    if (!removeTarget) return\n    onRemove?.(removeTarget.id)\n    setRemoveTarget(null)\n  }\n\n  return (\n    <div className=\"bg-card text-card-foreground mx-auto w-full max-w-2xl rounded-xl border shadow-sm\">\n      <div className=\"flex items-center justify-between border-b px-5 py-4\">\n        <div>\n          <h3 className=\"text-base font-semibold\">Saved cards</h3>\n          <p className=\"text-muted-foreground text-xs\">Manage the cards used for billing.</p>\n        </div>\n        <Button size=\"sm\" onClick={() => setShowAdd((v) => !v)}>\n          <Plus className=\"size-4\" /> Add card\n        </Button>\n      </div>\n\n      {/* Add form (collapses inline) */}\n      {showAdd ? (\n        <div className=\"bg-muted/30 max-h-[1000px] overflow-hidden border-b p-4 opacity-100 transition-all duration-300 ease-out\">\n          <PaymentForm\n            amount={0}\n            showWallets={false}\n            onSubmit={async (p) => {\n              onAdd?.(p as AddPayload)\n              setShowAdd(false)\n            }}\n          />\n        </div>\n      ) : null}\n\n      {/* Empty state */}\n      {cards.length === 0 && !showAdd ? (\n        <div className=\"px-6 py-14 text-center\">\n          <div className=\"bg-muted text-muted-foreground mx-auto grid size-12 place-items-center rounded-full\">\n            <Plus className=\"size-5\" />\n          </div>\n          <h4 className=\"mt-3 text-sm font-medium\">No cards saved</h4>\n          <p className=\"text-muted-foreground text-xs\">Add a card to use it for future payments.</p>\n          <Button size=\"sm\" className=\"mt-4\" onClick={() => setShowAdd(true)}>\n            Add your first card\n          </Button>\n        </div>\n      ) : (\n        /* List */\n        <ul className=\"divide-border divide-y\">\n          {cards.map((card) => (\n            <li\n              key={card.id}\n              className=\"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 className=\"min-w-0 flex-1\">\n                <div className=\"flex items-center gap-2\">\n                  <p className=\"text-sm font-medium\">\n                    {brandLabel(card.brand)} ending {card.last4}\n                  </p>\n                  {card.id === defaultId ? (\n                    <Badge variant=\"secondary\">\n                      <CheckCircle2 className=\"size-3\" /> Default\n                    </Badge>\n                  ) : null}\n                </div>\n                <p className=\"text-muted-foreground text-xs\">\n                  Expires {card.expiry} · {card.holder}\n                </p>\n              </div>\n              <div className=\"flex gap-1\">\n                {card.id !== defaultId ? (\n                  <Button variant=\"ghost\" size=\"sm\" onClick={() => onSetDefault?.(card.id)}>\n                    Set default\n                  </Button>\n                ) : null}\n                <Button\n                  variant=\"ghost\"\n                  size=\"icon-sm\"\n                  aria-label=\"Remove card\"\n                  onClick={() => setRemoveTarget(card)}\n                >\n                  <Trash2 className=\"text-muted-foreground size-4\" />\n                </Button>\n              </div>\n            </li>\n          ))}\n        </ul>\n      )}\n\n      {/* Remove confirmation */}\n      <Dialog open={confirmOpen} onOpenChange={(open) => !open && setRemoveTarget(null)}>\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\" onClick={() => setRemoveTarget(null)}>\n              Cancel\n            </Button>\n            <Button variant=\"destructive\" onClick={confirmRemove}>\n              Remove\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/SavedCardsList.tsx"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/payment-card.json",
    "https://uipkge.dev/r/react/payment-form.json",
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/badge.json",
    "https://uipkge.dev/r/react/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"
  ]
}