Oblivion
Oblivion
Docs
Go back
Component by
OV
Oblivion
Hover to reveal cards and balance
Linear
HolderELARA VANCE
**** 4242
Raycast
BusinessNOVA LABS
**** 8810
Oblivion
Accountdev@oblivion.sh
**** 0094
••••••
Total Assets
TSX
pocket-cards
"use client"

import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Eye, EyeOff, CreditCard } from 'lucide-react'
import { cn } from '@/lib/utils'

export const componentMeta = {
  name: "Pocket Cards",
  description: "A premium interactive wallet component with brand-shifting cards and a secure balance reveal. Inspired by tactile fintech interfaces.",
  props: {
    balance: { type: "string", description: "The total balance to display", default: '"$12,840.50"' },
    holderName: { type: "string", description: "Name on the primary cards", default: '"ELARA VANCE"' }
  }
}

export default function PocketCards({
  balance = "$12,840.50",
  holderName = "ELARA VANCE"
}: {
  balance?: string
  holderName?: string
}) {
  const [showBalance, setShowBalance] = useState(false)
  const [isWalletHovered, setIsWalletHovered] = useState(false)

  const cards = [
    {
      id: 'linear',
      brand: 'Linear',
      color: 'bg-[#5e6ad2]',
      textColor: 'text-white',
      label: 'Holder',
      value: holderName,
      number: '5524 9910 4242',
      stars: '**** 4242',
      delay: 0.1,
      hoverY: -75,
      rotate: -3,
      z: 10,
      activeY: -60
    },
    {
      id: 'raycast',
      brand: 'Raycast',
      color: 'bg-[#ff4f5e]',
      textColor: 'text-white',
      label: 'Business',
      value: 'NOVA LABS',
      number: '9012 4432 8810',
      stars: '**** 8810',
      delay: 0.2,
      hoverY: -45,
      rotate: 2,
      z: 20,
      activeY: -70
    },
    {
      id: 'oblivion',
      brand: 'Oblivion',
      color: 'bg-transparent',
      textColor: 'text-white',
      label: 'Account',
      value: 'dev@oblivion.sh',
      number: '3312 0045 0094',
      stars: '**** 0094',
      delay: 0.3,
      hoverY: -10,
      rotate: 0,
      z: 30,
      activeY: -60
    }
  ]

  return (
    <div className="flex items-center justify-center p-12 min-h-[400px]">
      <div 
        className="relative w-[280px] h-[230px] flex justify-center items-end group perspective-1000"
        onMouseEnter={() => setIsWalletHovered(true)}
        onMouseLeave={() => setIsWalletHovered(false)}
      >
        {/* Wallet Support Label */}
        <div className="absolute -bottom-8 group-hover:opacity-100 opacity-60 transition-all duration-300">
          <span className="text-[10px] font-bold uppercase tracking-widest text-zinc-500 flex items-center gap-2">
            <motion.span
              animate={{ x: [0, 4, 0] }}
              transition={{ repeat: Infinity, duration: 1.5 }}
            >

            </motion.span>
            Hover to reveal cards and balance
          </span>
        </div>

        {/* Wallet Back: Replaced with a more architectural 'Chamber' back */}
        <div className="absolute bottom-0 w-[280px] h-[210px] bg-transparent border border-white/5 rounded-[32px_32px_60px_60px] z-0 shadow-[inset_0_0_40px_rgba(0,0,0,0.8),0_30px_60px_-15px_rgba(0,0,0,1)] overflow-hidden">
             <div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(255,255,255,0.05)_0%,transparent_50%)]" />
             {/* Subtle linear grain */}
             <div className="absolute inset-0 opacity-[0.02] bg-[url('https://grainy-gradients.vercel.app/noise.svg')] pointer-events-none" />
        </div>

        {/* Cards Stack */}
        {cards.map((card, idx) => (
          <CardItem 
            key={card.id}
            card={card}
            isWalletHovered={isWalletHovered}
            bottom={90 - (idx * 25)}
          />
        ))}

        {/* New Leather-Glass Hybrid Pocket Front */}
        <motion.div 
          initial={false}
          className="absolute bottom-0 w-[280px] h-[170px] z-[45] rounded-[0_0_60px_60px] border-t border-white/10 flex flex-col items-center justify-start pt-10 overflow-hidden group/pocket"
          style={{
            background: "linear-gradient(180deg, rgba(24, 24, 27, 0.95) 0%, rgba(9, 9, 11, 1) 100%)",
            boxShadow: "0 -10px 40px -10px rgba(0,0,0,0.9), inset 0 1px 0px rgba(255,255,255,0.05)"
          }}
        >
          {/* High-Contrast Rim Light (The "Glow Edge") */}
          <div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
          
          {/* Leather Grain Texture Overlay */}
          <div className="absolute inset-0 opacity-[0.04] pointer-events-none bg-[url('https://grainy-gradients.vercel.app/noise.svg')]" />
          
          {/* Subtle Material Sheen */}
          <div className="absolute inset-0 bg-gradient-to-tr from-white/5 to-transparent pointer-events-none" />

          {/* Card Slot Accents (Deep Grooves) */}
          <div className="absolute top-3 left-1/2 -translate-x-1/2 w-12 h-1 bg-black/40 rounded-full blur-[1px]" />
          
          {/* Interactive Balance Area */}
          <div 
            className="relative z-50 flex flex-col items-center gap-1 cursor-pointer pointer-events-auto active:scale-95 transition-transform" 
            onClick={() => setShowBalance(!showBalance)}
          >
            <div className="relative h-8 w-full flex items-center justify-center">
              <AnimatePresence mode="wait">
                {showBalance || isWalletHovered ? (
                  <motion.div 
                    key="balance"
                    initial={{ opacity: 0, filter: 'blur(10px)', y: 5 }}
                    animate={{ opacity: 1, filter: 'blur(0px)', y: 0 }}
                    exit={{ opacity: 0, filter: 'blur(10px)', y: -5 }}
                    className="text-2xl font-bold text-white tracking-tight drop-shadow-lg"
                  >
                    {balance}
                  </motion.div>
                ) : (
                  <motion.div 
                    key="stars"
                    initial={{ opacity: 0 }}
                    animate={{ opacity: 1 }}
                    exit={{ opacity: 0 }}
                    className="text-2xl font-bold text-zinc-600 tracking-[8px] pt-1"
                  >
                    ••••••
                  </motion.div>
                )}
              </AnimatePresence>
            </div>
            
            <span className="text-[9px] font-black uppercase tracking-[0.2em] text-zinc-500/80">
              Total Assets
            </span>

            <motion.div 
              animate={{ opacity: isWalletHovered ? 1 : 0.4 }}
              className="mt-2 text-emerald-500/80"
            >
              {showBalance || isWalletHovered ? <Eye size={14} strokeWidth={3} /> : <EyeOff size={14} strokeWidth={3} />}
            </motion.div>
          </div>

          {/* Bottom Branding / Detail */}
          <div className="absolute bottom-6 left-1/2 -translate-x-1/2 w-48 h-[1px] bg-gradient-to-r from-transparent via-zinc-800 to-transparent" />
        </motion.div>
      </div>
    </div>
  )
}

function CardItem({ card, isWalletHovered, bottom }: { card: any, isWalletHovered: boolean, bottom: number }) {
  const [isHovered, setIsHovered] = useState(false)

  return (
    <motion.div
      initial={{ y: -100, opacity: 0 }}
      animate={{ 
        y: isHovered 
          ? card.activeY 
          : (isWalletHovered ? card.hoverY : 0),
        rotate: isHovered ? 0 : (isWalletHovered ? card.rotate : 0),
        scale: isHovered ? 1.05 : 1,
        opacity: 1,
        zIndex: isHovered ? 100 : card.z
      }}
      transition={{ 
        type: 'spring', 
        stiffness: 260, 
        damping: 20,
        delay: isWalletHovered ? 0 : card.delay 
      }}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      className={cn(
        "absolute w-[260px] h-[140px] left-[10px] rounded-2xl p-4 flex flex-col justify-between shadow-2xl shadow-black/50 border border-white/10 overflow-hidden",
        card.color,
        card.textColor
      )}
      style={{ bottom: `${bottom}px` }}
    >
      {/* Glossy Overlay */}
      <div className="absolute inset-0 bg-gradient-to-tr from-white/10 to-transparent pointer-events-none" />
      <div className="absolute -top-[100%] -left-[100%] w-[300%] h-[300%] bg-gradient-to-br from-white/5 via-transparent to-transparent rotate-45 pointer-events-none" />

      <div className="flex justify-between items-center relative z-10">
        <span className="text-xs font-black uppercase tracking-widest opacity-90">{card.brand}</span>
        <div className="w-8 h-6 bg-white/20 rounded-md border border-white/10 shadow-inner flex items-center justify-center">
           <div className="w-5 h-4 bg-yellow-500/20 rounded-[2px]" />
        </div>
      </div>

      <div className="flex justify-between items-end relative z-10">
        <div className="flex flex-col">
          <span className="text-[7px] uppercase tracking-[2px] opacity-60 mb-1">{card.label}</span>
          <span className="text-[10px] font-bold tracking-tight uppercase whitespace-nowrap">{card.value}</span>
        </div>
        
        <div className="text-right flex flex-col items-end">
          <AnimatePresence mode="wait">
            {isHovered ? (
              <motion.span 
                key="number"
                initial={{ opacity: 0, x: 10 }}
                animate={{ opacity: 1, x: 0 }}
                exit={{ opacity: 0, x: -10 }}
                className="text-[11px] font-mono tracking-widest"
              >
                {card.number}
              </motion.span>
            ) : (
              <motion.span 
                key="stars"
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                className="text-sm tracking-[2px] font-bold"
              >
                {card.stars}
              </motion.span>
            )}
          </AnimatePresence>
        </div>
      </div>
    </motion.div>
  )
}
Save to favoritesCopy to Figma
React

Pocket Cards

A premium interactive wallet component with brand-shifting cards and a secure balance reveal. Inspired by tactile fintech interfaces.

Installation

$ npx @cosmoo/oblivion add pocket-cards
Dependenciesnpm install framer-motion lucide-react

Usage Example

tsx
import PocketCards from "@/components/oblivion/pocket-cards"

export default function Demo() {
  return (
    <div className="p-10 flex justify-center">
      <PocketCards />
    </div>
  )
}