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
••••••
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>
)
}"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-cardsDependencies
npm install framer-motion lucide-reactUsage Example
tsx
import PocketCards from "@/components/oblivion/pocket-cards"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<PocketCards />
</div>
)
}import PocketCards from "@/components/oblivion/pocket-cards"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<PocketCards />
</div>
)
}