Oblivion
Oblivion
Docs
Go back
Component by
OV
Oblivion
Master Vault

Platinum Pulse

•••• •••• •••• 8842

Member since2026
Expiry date12/28
Master Vault

Neon Nexus

•••• •••• •••• 4421

Member since2026
Expiry date12/28
Master Vault

Cyber Slate

•••• •••• •••• 0098

Member since2026
Expiry date12/28
TSX
three-d-smartcards
"use client"

import { motion, useMotionValue, useSpring, useTransform, AnimatePresence } from "framer-motion"
import { useState, useEffect, useRef } from "react"
import { cn } from "@/lib/utils"

interface CardData {
  id: string
  title: string
  number: string
  gradient: string
  textColor: string
  secondaryColor: string
}

interface ThreeDSmartCardsProps {
  cards?: CardData[]
  className?: string
  autoPlay?: boolean
  interval?: number
}

export const componentMeta = {
  name: "3D Smartcards",
  description: "A premium 3D stacking card animation with holographic effects and smooth flipping transitions.",
  props: {
    cards: { type: "array", description: "Array of card data objects" },
    autoPlay: { type: "boolean", description: "Enable automatic flipping", default: "true" },
    interval: { type: "number", description: "Interval between flips (ms)", default: "3000" },
  }
}

const DEFAULT_CARDS: CardData[] = [
  {
    id: "card-1",
    title: "Platinum Pulse",
    number: "•••• •••• •••• 8842",
    gradient: "linear-gradient(135deg, #FF3D77 0%, #FF8BA0 100%)",
    secondaryColor: "#FFBDD0",
    textColor: "#FFFFFF",
  },
  {
    id: "card-2",
    title: "Neon Nexus",
    number: "•••• •••• •••• 4421",
    gradient: "linear-gradient(135deg, #6E45FA 0%, #9B7EFA 100%)",
    secondaryColor: "#C9BAFF",
    textColor: "#FFFFFF",
  },
  {
    id: "card-3",
    title: "Cyber Slate",
    number: "•••• •••• •••• 0098",
    gradient: "linear-gradient(135deg, #222222 0%, #444444 100%)",
    secondaryColor: "#888888",
    textColor: "#FFFFFF",
  },
]

const CardChip = () => (
  <div className="relative w-12 h-9 rounded-md overflow-hidden bg-gradient-to-br from-yellow-200 via-yellow-400 to-yellow-600 p-[1px] shadow-[0_2px_4px_rgba(0,0,0,0.3)]">
    <div className="w-full h-full bg-black/5 rounded-[5px] grid grid-cols-3 grid-rows-2 gap-[1px] p-[2px]">
      {[...Array(6)].map((_, i) => (
        <div key={i} className="bg-gradient-to-br from-yellow-50/70 to-transparent rounded-[1px]" />
      ))}
    </div>
    <div className="absolute top-1/2 left-0 w-full h-[0.5px] bg-black/30" />
    <div className="absolute top-0 left-1/2 w-[0.5px] h-full bg-black/30" />
    <div className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/20 to-transparent" />
  </div>
)

export default function ThreeDSmartCards({ 
  cards = DEFAULT_CARDS, 
  className,
  autoPlay = true,
  interval = 3500
}: ThreeDSmartCardsProps) {
  const [index, setIndex] = useState(0)
  const containerRef = useRef<HTMLDivElement>(null)
  
  // Mouse tracking for 3D tilt
  const mouseX = useMotionValue(0)
  const mouseY = useMotionValue(0)

  const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [15, -15]), { stiffness: 150, damping: 20 })
  const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-20, 20]), { stiffness: 150, damping: 20 })

  const handleMouseMove = (e: React.MouseEvent) => {
    if (!containerRef.current) return
    const rect = containerRef.current.getBoundingClientRect()
    const x = (e.clientX - rect.left) / rect.width - 0.5
    const y = (e.clientY - rect.top) / rect.height - 0.5
    mouseX.set(x)
    mouseY.set(y)
  }

  const handleMouseLeave = () => {
    mouseX.set(0)
    mouseY.set(0)
  }

  useEffect(() => {
    if (!autoPlay) return
    const timer = setInterval(() => {
      setIndex((prev) => (prev + 1) % cards.length)
    }, interval)
    return () => clearInterval(timer)
  }, [autoPlay, interval, cards.length])

  return (
    <div 
      ref={containerRef}
      className={cn("relative w-[600px] h-[450px] flex items-center justify-center", className)}
      style={{ perspective: "2000px" }}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
    >
      <motion.div
        style={{
          rotateX,
          rotateY,
          transformStyle: "preserve-3d",
        }}
        className="relative w-[360px] h-[225px]"
      >
        <AnimatePresence mode="popLayout">
          {cards.map((card, i) => {
            const relativeIndex = (i - index + cards.length) % cards.length
            const isTop = relativeIndex === 0
            
            return (
              <motion.div
                key={card.id}
                initial={{ 
                  scale: 0.8, 
                  opacity: 0, 
                  z: -relativeIndex * 80,
                  x: 60,
                  y: 40
                }}
                animate={{ 
                  scale: 1 - relativeIndex * 0.05,
                  opacity: 1,
                  z: -relativeIndex * 80,
                  y: relativeIndex * 10,
                  x: isTop ? -50 : relativeIndex * 40,
                  rotateY: isTop ? -30 : -12,
                  rotateZ: relativeIndex * -1,
                }}
                exit={{ 
                  x: 300, 
                  opacity: 0, 
                  scale: 0.7,
                  z: 200,
                  rotateY: 60,
                  transition: { duration: 0.8, ease: [0.23, 1, 0.32, 1] }
                }}
                transition={{
                  type: "spring",
                  stiffness: 90,
                  damping: 15,
                  mass: 1.2
                }}
                style={{
                  position: "absolute",
                  inset: 0,
                  background: card.gradient,
                  borderRadius: 32,
                  padding: 32,
                  paddingBottom: 64, // Massive bottom padding to prevent clipping
                  color: card.textColor,
                  overflow: "hidden",
                  boxShadow: `
                    ${isTop ? '0 50px 100px -20px rgba(0,0,0,0.6)' : '0 15px 40px -10px rgba(0,0,0,0.4)'}, 
                    inset 0 0 0 1px ${card.secondaryColor}55,
                    inset 0 2px 0 0 rgba(255,255,255,0.3),
                    inset 0 -2px 0 0 rgba(0,0,0,0.3)
                  `,
                  transformStyle: "preserve-3d",
                  zIndex: cards.length - relativeIndex,
                  backfaceVisibility: "hidden",
                }}
                className="cursor-pointer group"
                onClick={() => setIndex((i + 1) % cards.length)}
              >
                {/* Brushed Metal Texture Overlay */}
                <div className="absolute inset-0 opacity-[0.03] pointer-events-none bg-[url('https://grainy-gradients.vercel.app/noise.svg')] brightness-150 mix-blend-overlay" />

                {/* Metallic Chrome Shine - Top Card Only */}
                {isTop && (
                  <motion.div 
                    className="absolute inset-0 rounded-[32px] overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none"
                    style={{
                      background: "linear-gradient(110deg, transparent 15%, rgba(255,255,255,0.05) 20%, rgba(255,255,255,0.4) 35%, rgba(255,255,255,0.4) 45%, rgba(255,255,255,0.05) 60%, transparent 65%)",
                      backgroundSize: "250% 100%",
                      mixBlendMode: "overlay"
                    }}
                    animate={{
                      backgroundPosition: ["200% 0%", "-200% 0%"],
                    }}
                    transition={{
                      duration: 4,
                      repeat: Infinity,
                      ease: "linear"
                    }}
                  />
                )}

                {/* Chiseled Rim Light */}
                <div 
                  className="absolute inset-0 pointer-events-none rounded-[32px] border border-white/20"
                  style={{
                    background: isTop 
                      ? "linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 30%, rgba(0,0,0,0.2) 100%)"
                      : "linear-gradient(to bottom, rgba(255,255,255,0.1), rgba(0,0,0,0.3))",
                  }}
                />

                {/* 3D Translated Content - Fully visible on background cards but slightly darker */}
                <div 
                  className={cn(
                    "w-full h-full flex flex-col justify-between relative z-10 select-none transition-all duration-500",
                    !isTop && "brightness-[0.6] saturate-[0.8]"
                  )}
                  style={{ transform: `translateZ(${isTop ? 60 : 30}px)` }}
                >
                  <div className="flex justify-between items-start">
                    <div className="flex flex-col gap-1 drop-shadow-2xl">
                      <div className="flex items-center gap-2">
                         <div className="w-1.5 h-1.5 rounded-full bg-white shadow-[0_0_8px_#fff]" />
                         <span className="text-[11px] font-black uppercase tracking-[0.3em] text-white/80">Master Vault</span>
                      </div>
                      <h3 className="text-3xl font-black tracking-tighter italic text-white drop-shadow-[0_4px_4px_rgba(0,0,0,0.5)]">{card.title}</h3>
                    </div>
                    <CardChip />
                  </div>

                  {/* Bottom Details - Heavily pushed up to prevent edge clipping */}
                  <div className="mt-auto flex flex-col gap-6 -translate-y-4">
                    <p className="text-2xl font-mono tracking-[0.2em] text-white/90 drop-shadow-[0_2px_10px_rgba(0,0,0,0.8)] font-bold">{card.number}</p>
                    <div className="flex justify-between items-end">
                      <div className="flex gap-8">
                        <div className="flex flex-col">
                          <span className="text-[9px] font-black uppercase tracking-widest text-white/60">Member since</span>
                          <span className="text-base font-black tabular-nums text-white">2026</span>
                        </div>
                        <div className="flex flex-col">
                          <span className="text-[9px] font-black uppercase tracking-widest text-white/60">Expiry date</span>
                          <span className="text-base font-black tabular-nums text-white">12/28</span>
                        </div>
                      </div>
                      <div className="relative w-14 h-14 flex items-center justify-center">
                         <div className="absolute inset-0 rounded-full bg-white/5 backdrop-blur-2xl border border-white/30 shadow-inner" />
                         <div className="w-9 h-9 rounded-full border-[4px] border-white/60 relative overflow-hidden shadow-2xl">
                            <div className="absolute top-0 -left-full w-full h-full bg-white/30 skew-x-[45deg] animate-pulse" />
                         </div>
                      </div>
                    </div>
                  </div>
                </div>
              </motion.div>
            )
          })}
        </AnimatePresence>
      </motion.div>
    </div>



  )
}
Save to favoritesCopy to Figma
React

3D Smartcards

A premium 3D stacking card animation with holographic effects and smooth flipping transitions.

Installation

$ npx @cosmoo/oblivion add three-d-smartcards
Dependenciesnpm install framer-motion

Usage Example

tsx
import ThreeDSmartcards from "@/components/oblivion/three-d-smartcards"

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