Menu

Change language

GitHub repo

Payment Card

payment-cardui

Premium metallic credit-card visual with embossed type, holographic foil, and per-digit typing animation. Auto-detects brand (Visa, Mastercard, Amex, Discover), flips in 3D, and supports opt-in mouse-parallax tilt and shimmer. Pure presentational — pass typed values in via props.

Installation

$npx shadcn@latest add https://uipkge.dev/r/lit/payment-card.json
Manifest:https://uipkge.dev/r/lit/payment-card.jsonInstalls to:src/uip/components/payment-card/

Examples

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
numberstring—optional
namestring—optional
expirystring—optional
cvcstring—optional
brandCardBrand | 'auto'autooptional
flippedbooleanfalseoptional
variant'default' | 'compact'defaultoptional
tiltbooleanfalseoptional
shimmerbooleanfalseoptional
flipstringtrueoptional
size'sm' | 'md' | 'lg'mdoptional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

BrandVisual
interface BrandVisual {
  cardClass: string
  accentGlow: string
  foilSheen: string
  guillocheColor: string
  textColor: string
  subtextColor: string
  embossStyle: string
  chipType: 'gold' | 'silver'
}
HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-payment-card': UipPaymentCard
  }
}

npm dependencies

Includes

Used by

Files installed (1)

  • src/uip/components/payment-card/payment-card.ts22.3 kB
    import { LitElement, css, html, nothing, svg } from 'lit'
    import { styleMap } from 'lit/directives/style-map.js'
    import { cn } from '../../lib/utils'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    
    export type CardBrand = 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown'
    
    // --- verbatim from packages/registry-react/components/payment-card/payment-card.tsx ---
    function detectBrand(raw: string): CardBrand {
      const n = raw.replace(/\D/g, '')
      if (!n) return 'unknown'
      if (/^4/.test(n)) return 'visa'
      if (/^(5[1-5]|2[2-7])/.test(n)) return 'mastercard'
      if (/^3[47]/.test(n)) return 'amex'
      if (/^(6011|65|64[4-9])/.test(n)) return 'discover'
      return 'unknown'
    }
    
    function formatMasked(raw: string, brand: CardBrand): string {
      const digits = raw.replace(/\D/g, '').slice(0, brand === 'amex' ? 15 : 16)
      const groups = brand === 'amex' ? [4, 6, 5] : [4, 4, 4, 4]
      const total = groups.reduce((a, b) => a + b, 0)
      const padded = (digits + '••••••••••••••••').slice(0, total)
      let i = 0
      return groups
        .map((g) => {
          const slice = padded.slice(i, i + g)
          i += g
          return slice
        })
        .join(' ')
    }
    
    interface BrandVisual {
      cardClass: string
      accentGlow: string
      foilSheen: string
      guillocheColor: string
      textColor: string
      subtextColor: string
      embossStyle: string
      chipType: 'gold' | 'silver'
    }
    
    const BRAND_VISUALS: Record<CardBrand, BrandVisual> = {
      visa: {
        cardClass:
          'bg-[radial-gradient(ellipse_at_top_right,#1e3a8a_0%,#0f2252_45%,#08122c_85%,#040a18_100%)] text-white shadow-[0_22px_45px_-12px_rgba(15,34,82,0.65),0_8px_16px_-8px_rgba(0,0,0,0.5)]',
        accentGlow: 'from-blue-400/20 via-indigo-500/10 to-transparent',
        foilSheen: 'from-white/20 via-blue-200/5 to-transparent',
        guillocheColor: 'rgba(96, 165, 250, 0.12)',
        textColor: 'text-white',
        subtextColor: 'text-blue-200/60',
        embossStyle: 'text-white drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)]',
        chipType: 'gold',
      },
      mastercard: {
        cardClass:
          'bg-[radial-gradient(ellipse_at_top_right,#2a0808_0%,#180608_35%,#0f0506_70%,#09090b_100%)] text-white shadow-[0_22px_45px_-12px_rgba(220,38,38,0.35),0_8px_16px_-8px_rgba(0,0,0,0.6)]',
        accentGlow: 'from-red-500/25 via-amber-500/15 to-transparent',
        foilSheen: 'from-white/20 via-orange-300/10 to-transparent',
        guillocheColor: 'rgba(239, 68, 68, 0.1)',
        textColor: 'text-white',
        subtextColor: 'text-zinc-400',
        embossStyle: 'text-zinc-100 drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]',
        chipType: 'gold',
      },
      amex: {
        cardClass:
          'bg-[radial-gradient(ellipse_at_top_right,#1e293b_0%,#0f172a_40%,#090d16_80%,#020617_100%)] text-slate-100 shadow-[0_22px_45px_-12px_rgba(15,23,42,0.7),0_8px_16px_-8px_rgba(0,0,0,0.6)]',
        accentGlow: 'from-slate-300/20 via-teal-500/10 to-transparent',
        foilSheen: 'from-white/25 via-cyan-100/10 to-transparent',
        guillocheColor: 'rgba(148, 163, 184, 0.12)',
        textColor: 'text-slate-100',
        subtextColor: 'text-slate-400',
        embossStyle: 'text-slate-100 drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]',
        chipType: 'silver',
      },
      discover: {
        cardClass:
          'bg-[radial-gradient(ellipse_at_top_right,#431407_0%,#270d04_45%,#1c0903_80%,#0c0401_100%)] text-white shadow-[0_22px_45px_-12px_rgba(194,65,12,0.45),0_8px_16px_-8px_rgba(0,0,0,0.6)]',
        accentGlow: 'from-orange-400/25 via-amber-600/15 to-transparent',
        foilSheen: 'from-white/20 via-amber-200/10 to-transparent',
        guillocheColor: 'rgba(251, 146, 60, 0.12)',
        textColor: 'text-white',
        subtextColor: 'text-amber-200/60',
        embossStyle: 'text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]',
        chipType: 'gold',
      },
      unknown: {
        cardClass:
          'bg-[radial-gradient(ellipse_at_top_right,#27272a_0%,#18181b_40%,#09090b_85%,#000000_100%)] text-white shadow-[0_22px_45px_-12px_rgba(0,0,0,0.7),0_8px_16px_-8px_rgba(0,0,0,0.5)]',
        accentGlow: 'from-zinc-400/15 via-zinc-600/10 to-transparent',
        foilSheen: 'from-white/18 via-zinc-300/5 to-transparent',
        guillocheColor: 'rgba(255, 255, 255, 0.06)',
        textColor: 'text-white',
        subtextColor: 'text-zinc-400',
        embossStyle: 'text-zinc-100 drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]',
        chipType: 'silver',
      },
    }
    
    // --- end verbatim ---
    
    // Boolean props whose React default is `true`: `attr="false"` turns them off.
    const trueByDefault = { fromAttribute: (v: string | null) => v !== 'false', toAttribute: () => null }
    
    /**
     * <uip-payment-card> — the registry PaymentCard (a display-only credit-card
     * preview, `role="img"`) as a web component. Not a form control in React, so
     * not form-associated here either; pair it with inputs that format the values.
     *
     * Class strings, brand visuals, masking (`formatMasked`), brand detection and
     * the tilt maths are React's verbatim. The React root <div> (role=img, size
     * width, perspective, mouse handlers) is rendered inside the shadow root; the
     * host is `display: inline-block` like that root's `inline-block`.
     *
     * Properties / attributes (React prop names): `number`, `name`, `expiry`,
     * `cvc`, `brand` ('auto' | 'visa' | 'mastercard' | 'amex' | 'discover' |
     * 'unknown'), `flipped`, `variant` ('default' | 'compact'), `tilt`, `shimmer`,
     * `flip` (default true; `flip="false"` for the short 300ms transition),
     * `size` ('sm' | 'md' | 'lg').
     *
     * No events (React has no callbacks).
     */
    export class UipPaymentCard extends LitElement {
      static styles = [tailwind, css`:host { display: inline-block; }`]
    
      static properties = {
        number: {},
        name: {},
        expiry: {},
        cvc: {},
        brand: { reflect: true },
        flipped: { type: Boolean, reflect: true },
        variant: { reflect: true },
        tilt: { type: Boolean },
        shimmer: { type: Boolean },
        flip: { converter: trueByDefault },
        size: { reflect: true },
        tiltState: { state: true },
      }
    
      number = ''
      name = ''
      expiry = ''
      cvc = ''
      brand: CardBrand | 'auto' = 'auto'
      flipped = false
      variant: 'default' | 'compact' = 'default'
      tilt = false
      shimmer = false
      flip = true
      size: 'sm' | 'md' | 'lg' = 'md'
      private tiltState = { x: 0, y: 0, active: false }
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'payment-card')
      }
    
      private get cardEl() {
        return this.renderRoot?.querySelector<HTMLDivElement>('[role=img]')
      }
    
      private onMouseMove = (e: MouseEvent) => {
        const el = this.cardEl
        if (!this.tilt || !el) return
        const rect = el.getBoundingClientRect()
        const x = e.clientX - rect.left
        const y = e.clientY - rect.top
        const px = (x / rect.width - 0.5) * 2
        const py = (y / rect.height - 0.5) * 2
        this.tiltState = { x: -py * 10, y: px * 12, active: true }
      }
    
      private onMouseLeave = () => {
        if (!this.tilt) return
        this.tiltState = { x: 0, y: 0, active: false }
      }
    
      private renderBrandLogo(detectedBrand: CardBrand) {
        const compact = this.variant === 'compact'
        switch (detectedBrand) {
          case 'visa':
            return html`<svg
              class=${compact ? 'h-3 w-auto' : 'h-6 w-auto'}
              viewBox="0 0 78 24"
              fill="none"
              xmlns="http://www.w3.org/2000/svg"
              aria-label="Visa"
            >
              <path
                d="M29.5 23.2H23.9L27.4 1.8H33L29.5 23.2ZM52.4 2.3C51.3 1.9 49.5 1.5 47.3 1.5C41.8 1.5 37.9 4.4 37.8 8.6C37.7 11.7 40.5 13.4 42.6 14.4C44.8 15.5 45.5 16.2 45.5 17.1C45.5 18.6 43.7 19.2 42.1 19.2C39.8 19.2 38.5 18.9 36.8 18.1L36 17.7L35.1 23C36.6 23.7 39.2 24.2 41.8 24.2C47.7 24.2 51.5 21.3 51.6 16.9C51.7 13.4 49.6 11.7 47 10.4C45.2 9.5 44.1 8.9 44.1 8C44.1 7.2 45 6.4 46.8 6.4C48.3 6.4 49.6 6.7 50.8 7.2L51.3 7.5L52.4 2.3ZM67.5 1.8H63.2C61.8 1.8 60.8 2.2 60.2 3.6L51.4 23.2H57.3L58.5 19.9H65.7L66.4 23.2H71.6L67.5 1.8ZM60.1 15.5C60.6 14.2 62.5 8.9 62.5 8.9C62.5 8.9 63 7.6 63.3 6.7L63.8 8.7C63.8 8.7 64.9 14.1 65.2 15.5H60.1ZM18.7 1.8L13.2 16.4L12.6 13.4C11.6 10 8.7 6.3 5.4 4.5L10.3 23.2H16.3L25.1 1.8H18.7ZM8.3 1.8H0.2L0 2.3C6.3 3.9 10.4 7.7 12.1 12.4L10.4 3.7C10.1 2.4 9.3 1.9 8.3 1.8Z"
                fill="white"
              />
              <path d="M12.1 12.4L10.4 3.7C10.1 2.4 9.3 1.9 8.3 1.8H0.2L0 2.3C6.3 3.9 10.4 7.7 12.1 12.4Z" fill="#F9A01B" />
            </svg>`
          case 'mastercard':
            return html`<div class="flex items-center gap-1.5">
              <svg
                class=${compact ? 'h-3.5 w-auto' : 'h-7 w-auto'}
                viewBox="0 0 48 30"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
                aria-label="Mastercard"
              >
                <circle cx="15" cy="15" r="15" fill="#EB001B" />
                <circle cx="33" cy="15" r="15" fill="#F79E1B" fill-opacity="0.92" />
                <path
                  d="M24 4.5C27.5 7.1 29.8 11.2 29.8 16C29.8 20.8 27.5 24.9 24 27.5C20.5 24.9 18.2 20.8 18.2 16C18.2 11.2 20.5 7.1 24 4.5Z"
                  fill="#FF5F00"
                />
              </svg>
              ${!compact
                ? // React sets letter-spacing -0.05em inline; a static inline style is
                  // expressed as an arbitrary-property utility here.
                  html`<span class="font-mono text-xs font-bold tracking-tight text-white lowercase [letter-spacing:-0.05em]">mastercard</span>`
                : nothing}
            </div>`
          case 'amex':
            return html`<div
              class=${cn(
                'flex items-center justify-center rounded-sm bg-[#006fcf] font-sans font-bold tracking-tighter text-white shadow-xs',
                compact ? 'px-1 py-0.5 text-xs' : 'px-2 py-1 text-xs',
              )}
            >
              AMEX
            </div>`
          case 'discover':
            return html`<div class=${cn('flex items-center gap-0.5 font-bold tracking-wider text-white', compact ? 'text-xs' : 'text-xs')}>
              <span>DISC</span>
              <span
                class=${cn(
                  'inline-block rounded-full bg-gradient-to-br from-amber-400 to-orange-600 shadow-xs',
                  compact ? 'size-2' : 'size-3.5',
                )}
              ></span>
              <span>VER</span>
            </div>`
          default:
            return html`<div class="flex items-center gap-1 opacity-80">
              <div class="size-2 rounded-full bg-white/60"></div>
              <div class="size-2 rounded-full bg-white/30"></div>
              <span class=${cn('font-mono font-bold tracking-widest text-white/80 uppercase', compact ? 'text-xs' : 'text-xs')}>PREMIUM</span>
            </div>`
        }
      }
    
      render() {
        const compact = this.variant === 'compact'
        const detectedBrand: CardBrand = this.brand && this.brand !== 'auto' ? this.brand : detectBrand(this.number ?? '')
        const theme = BRAND_VISUALS[detectedBrand] ?? BRAND_VISUALS.unknown
    
        const displayNumber = formatMasked(this.number ?? '', detectedBrand)
        const displayName = ((this.name ?? '').trim() || 'CARDHOLDER NAME').toUpperCase()
        const displayExpiry = (() => {
          const raw = (this.expiry ?? '').replace(/\D/g, '').slice(0, 4)
          if (!raw) return 'MM/YY'
          if (raw.length <= 2) return raw + '/YY'
          return `${raw.slice(0, 2)}/${raw.slice(2, 4)}`
        })()
        const displayCvc = (() => {
          const max = detectedBrand === 'amex' ? 4 : 3
          const digits = (this.cvc ?? '').replace(/\D/g, '').slice(0, max)
          return digits || '•••'
        })()
        const displayChars = displayNumber.split('')
    
        const sizeClass = compact
          ? 'w-[120px]'
          : this.size === 'sm'
            ? 'w-[280px]'
            : this.size === 'lg'
              ? 'w-[420px]'
              : 'w-[360px]'
    
        const flipDeg = this.flipped ? 180 : 0
        const innerTransform =
          this.tilt && this.tiltState.active
            ? `perspective(1200px) rotateX(${this.tiltState.x}deg) rotateY(${this.tiltState.y + flipDeg}deg) scale3d(1.02, 1.02, 1.02)`
            : `perspective(1200px) rotateY(${flipDeg}deg)`
    
        const bevel = html`<div
          class="pointer-events-none absolute inset-0 rounded-[16px] shadow-[inset_0_1px_1px_rgba(255,255,255,0.35),inset_0_-1px_1px_rgba(0,0,0,0.5)]"
        ></div>`
    
        return html`<div
          part="base"
          role="img"
          aria-label=${`${detectedBrand === 'unknown' ? 'Card' : detectedBrand} ending ${displayNumber.replace(/[•\s]/g, '').slice(-4) || '••••'}, ${displayName}, expires ${displayExpiry}`}
          class=${cn('group relative inline-block select-none [perspective:1200px]', sizeClass)}
          @mousemove=${this.onMouseMove}
          @mouseleave=${this.onMouseLeave}
        >
          <div
            class=${cn(
              'relative aspect-[85.6/53.98] w-full transition-transform [transform-style:preserve-3d]',
              this.flip ? 'duration-[650ms] ease-[cubic-bezier(0.22,1,0.36,1)]' : 'duration-300',
            )}
            style=${styleMap({ transform: innerTransform })}
          >
            <!-- FRONT FACE -->
            <div
              class=${cn(
                'pay-card-face absolute inset-0 overflow-hidden rounded-[16px] border border-white/15 [backface-visibility:hidden]',
                theme.cardClass,
              )}
            >
              <svg
                class="pointer-events-none absolute inset-0 size-full opacity-60 mix-blend-screen"
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 360 227"
                fill="none"
                aria-hidden="true"
              >
                ${svg`<g stroke=${theme.guillocheColor} stroke-width="0.75" stroke-opacity="0.7">
                  <path d="M-40 40 C 60 180, 180 -40, 400 120" />
                  <path d="M-40 55 C 60 195, 180 -25, 400 135" />
                  <path d="M-40 70 C 60 210, 180 -10, 400 150" />
                  <path d="M-40 85 C 60 225, 180 5, 400 165" />
                  <path d="M-40 100 C 60 240, 180 20, 400 180" />
                  <path d="M-40 115 C 60 255, 180 35, 400 195" />
                  <circle cx="280" cy="80" r="90" stroke-width="0.5" stroke-dasharray="2 3" />
                  <circle cx="280" cy="80" r="60" stroke-width="0.5" stroke-dasharray="1 2" />
                </g>`}
              </svg>
    
              <div class="pointer-events-none absolute inset-0 bg-gradient-to-tr from-transparent via-white/[0.07] to-transparent opacity-80"></div>
              <div class=${cn('pointer-events-none absolute -inset-y-8 -left-1/3 w-1/2 rotate-12 bg-gradient-to-r opacity-70', theme.foilSheen)}></div>
              <div class=${cn('pointer-events-none absolute -top-12 -right-12 h-44 w-44 rounded-full bg-gradient-to-br blur-2xl', theme.accentGlow)}></div>
              <div class="pointer-events-none absolute -bottom-10 -left-10 h-36 w-36 rounded-full bg-black/40 blur-2xl"></div>
              ${bevel}
              ${this.shimmer
                ? // React sets animation: payment-card-shimmer 2.5s linear infinite inline
                  // (the keyframes are not defined in the React registry either).
                  html`<div
                    class="pointer-events-none absolute -inset-x-full inset-y-0 bg-gradient-to-r from-transparent via-white/25 to-transparent [animation:payment-card-shimmer_2.5s_linear_infinite]"
                  ></div>`
                : nothing}
    
              <div class=${cn('absolute inset-0 flex flex-col justify-between', compact ? 'p-2' : 'p-5')}>
                <!-- TOP ROW -->
                <div class="flex items-center justify-between">
                  <div class="flex items-center gap-1.5">
                    ${!compact
                      ? html`<span class="font-mono text-xs font-semibold tracking-[0.25em] text-white/70 uppercase">${detectedBrand === 'amex'
                          ? 'PLATINUM'
                          : detectedBrand === 'mastercard'
                            ? 'WORLD ELITE'
                            : 'SIGNATURE'}</span>`
                      : html`<span class="font-mono text-xs font-bold tracking-widest text-white/70 uppercase">CARD</span>`}
                  </div>
                  <div class="flex items-center justify-end">${this.renderBrandLogo(detectedBrand)}</div>
                </div>
    
                <!-- MIDDLE ROW -->
                <div class="flex items-center justify-between">
                  <div class="flex items-center gap-3">
                    <div
                      class=${cn(
                        'relative overflow-hidden rounded-[5px] border border-amber-950/40 shadow-[0_2px_5px_rgba(0,0,0,0.5),inset_0_1px_0_rgba(255,255,255,0.6)]',
                        theme.chipType === 'silver'
                          ? 'bg-gradient-to-br from-slate-200 via-slate-300 to-slate-400'
                          : 'bg-gradient-to-br from-[#ffe082] via-[#ffb300] to-[#ff8f00]',
                        compact ? 'h-3.5 w-5' : 'h-8 w-11',
                      )}
                    >
                      <div class="absolute inset-[2px] rounded-[3px] border border-black/25"></div>
                      <div class="absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-black/30"></div>
                      <div class="absolute inset-y-0 left-1/3 w-px bg-black/30"></div>
                      <div class="absolute inset-y-0 right-1/3 w-px bg-black/30"></div>
                      <div class="absolute top-1/4 right-1/3 left-1/3 h-1/2 rounded-[2px] border border-black/25 bg-black/10"></div>
                      <div class="pointer-events-none absolute inset-0 bg-gradient-to-tr from-transparent via-white/40 to-transparent"></div>
                    </div>
                    ${!compact
                      ? html`<svg
                          viewBox="0 0 24 24"
                          class="h-5 w-auto text-white/70 drop-shadow-sm"
                          fill="none"
                          stroke="currentColor"
                          stroke-width="1.75"
                          stroke-linecap="round"
                          aria-hidden="true"
                        >
                          <path d="M6.5 8a6 6 0 0 1 0 8" />
                          <path d="M10 6a9.5 9.5 0 0 1 0 12" />
                          <path d="M13.5 4a13 13 0 0 1 0 16" />
                        </svg>`
                      : nothing}
                  </div>
                  ${!compact ? html`<div class="pay-card-holo h-7 w-9 rounded-[4px] opacity-75 shadow-xs"></div>` : nothing}
                </div>
    
                <!-- CARD NUMBER -->
                <div
                  class=${cn(
                    'flex flex-nowrap items-center font-mono font-bold tracking-[0.18em] tabular-nums',
                    theme.embossStyle,
                    compact ? 'my-0.5 text-xs' : 'my-1 text-base sm:text-lg',
                  )}
                >
                  <span class="sr-only">${displayNumber}</span>
                  ${displayChars.map(
                    (ch) => html`<span
                      class=${cn('inline-block origin-bottom tabular-nums transition-transform', ch === '•' && 'text-[0.85em] opacity-50')}
                      aria-hidden="true"
                    >${ch === ' ' ? ' ' : ch}</span>`,
                  )}
                </div>
    
                <!-- BOTTOM ROW -->
                <div class="flex items-end justify-between gap-3">
                  <div class="min-w-0 flex-1">
                    ${!compact
                      ? html`<div class=${cn('text-xs font-semibold tracking-[0.2em] uppercase', theme.subtextColor)}>CARDHOLDER NAME</div>`
                      : nothing}
                    <div class=${cn('truncate font-semibold tracking-wider uppercase', theme.embossStyle, compact ? 'text-xs' : 'text-xs')}>${displayName}</div>
                  </div>
                  <div class="shrink-0 text-right">
                    ${!compact
                      ? html`<div class=${cn('flex items-center justify-end gap-1 text-xs font-semibold tracking-wider uppercase', theme.subtextColor)}>
                          <span class="text-[6px] leading-tight">GOOD<br />THRU</span>
                          <span>MONTH/YEAR</span>
                        </div>`
                      : nothing}
                    <div class=${cn('font-mono font-semibold tracking-wider whitespace-nowrap', theme.embossStyle, compact ? 'text-xs' : 'text-xs')}>${displayExpiry}</div>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- BACK FACE -->
            <div
              class=${cn(
                'pay-card-face absolute inset-0 [transform:rotateY(180deg)] overflow-hidden rounded-[16px] border border-white/15 [backface-visibility:hidden]',
                theme.cardClass,
              )}
            >
              ${bevel}
              <div
                class=${cn(
                  'w-full bg-gradient-to-b from-neutral-950 via-black to-neutral-900 shadow-[inset_0_1px_0_rgba(255,255,255,0.1),inset_0_-1px_0_rgba(0,0,0,0.8)]',
                  compact ? 'mt-2 h-4' : 'mt-4 h-10',
                )}
              ></div>
              <div class=${cn('relative flex flex-col justify-between', compact ? 'p-2' : 'p-4')}>
                <div class=${cn('flex items-stretch gap-2', compact ? 'mt-1' : 'mt-2')}>
                  <!-- React's inline backgroundImage as an arbitrary-value utility. -->
                  <div
                    class=${cn(
                      'relative flex-1 overflow-hidden rounded-[3px] border border-slate-300/80 bg-white/95 px-2 py-1',
                      compact ? 'h-4' : 'h-8',
                      'bg-[repeating-linear-gradient(-45deg,rgba(0,0,0,0.05)_0px,rgba(0,0,0,0.05)_2px,transparent_2px,transparent_6px)]',
                    )}
                  >
                    ${!compact
                      ? html`<div class="font-mono text-xs font-semibold tracking-wider text-slate-400 uppercase">AUTHORIZED SIGNATURE</div>`
                      : nothing}
                  </div>
                  <div
                    class=${cn(
                      'flex flex-col items-center justify-center rounded-[3px] border border-slate-300 bg-white font-mono font-bold text-slate-950 shadow-inner',
                      compact ? 'px-1 text-xs' : 'px-3 text-xs',
                    )}
                  >
                    ${!compact ? html`<span class="text-[6px] font-bold tracking-widest text-slate-400 uppercase">CVV</span>` : nothing}
                    <span class="leading-none tracking-widest italic">${displayCvc}</span>
                  </div>
                </div>
                ${!compact
                  ? html`<div class="mt-3 space-y-1.5">
                      <p class="text-[7.5px] leading-snug text-white/50">
                        This card is property of the issuing bank and must be returned upon request. Use is subject to the
                        cardholder agreement.
                      </p>
                      <div class="flex items-center justify-between border-t border-white/10 pt-2 text-xs font-medium text-white/60">
                        <span>24/7 Support: 1-800-555-0199</span>
                        <span class="font-mono font-bold tracking-wider text-white/75">CIRRUS · PLUS · STAR</span>
                      </div>
                    </div>`
                  : nothing}
              </div>
            </div>
          </div>
        </div>`
      }
    }
    
    customElements.get('uip-payment-card') || customElements.define('uip-payment-card', UipPaymentCard)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-payment-card': UipPaymentCard
      }
    }
    

Raw manifest:https://uipkge.dev/r/lit/payment-card.json