Spotlight Product Card
A premium e-commerce card with 3D tilt, spotlight hover effect, and expandable actions. Features smooth spring physics and color selection.
Installation
Add this component to your project using the CLI:
terminal
npx vui-registry-cli-v1 add spotlight-product-cardSource Code
spotlight-product-card.tsx
'use client'
import React, { useRef, useState } from 'react'
import { motion, useMotionTemplate, useMotionValue, useSpring, useTransform } from 'framer-motion'
import { ShoppingBag, Star, ArrowRight, Heart } from 'lucide-react'
import { cn } from '@/lib/utils'
export interface ProductData {
id: string
name: string
category: string
price: number
rating: number
image: string
colors: string[]
}
const DEFAULT_PRODUCT: ProductData = {
id: '1',
name: 'Velocity Air Max',
category: 'Premium Footwear',
price: 189.00,
rating: 4.8,
image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=2070&auto=format&fit=crop',
colors: ['#ef4444', '#3b82f6', '#10b981', '#171717']
}
interface SpotlightProductCardProps {
product?: ProductData;
onAddToCart?: (product: ProductData, color: string) => void;
onToggleFavorite?: (product: ProductData) => void;
}
export default function SpotlightProductCard({
product = DEFAULT_PRODUCT,
onAddToCart,
onToggleFavorite
}: SpotlightProductCardProps) {
return (
<div className="flex items-center justify-center min-h-[600px] bg-neutral-100 dark:bg-neutral-950 p-8 perspective-1000">
<ProductCard
product={product}
onAddToCart={onAddToCart}
onToggleFavorite={onToggleFavorite}
/>
</div>
)
}
function ProductCard({
product,
onAddToCart,
onToggleFavorite
}: {
product: ProductData;
onAddToCart?: (product: ProductData, color: string) => void;
onToggleFavorite?: (product: ProductData) => void;
}) {
const ref = useRef<HTMLDivElement>(null)
const [selectedColor, setSelectedColor] = useState(product.colors[0])
const [isHovered, setIsHovered] = useState(false)
const [isFavorite, setIsFavorite] = useState(false)
// Mouse position logic
const x = useMotionValue(0)
const y = useMotionValue(0)
// Smooth mouse motion
const mouseXSpring = useSpring(x)
const mouseYSpring = useSpring(y)
const rotateX = useTransform(mouseYSpring, [-0.5, 0.5], ["15deg", "-15deg"])
const rotateY = useTransform(mouseXSpring, [-0.5, 0.5], ["-15deg", "15deg"])
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!ref.current) return
const rect = ref.current.getBoundingClientRect()
const width = rect.width
const height = rect.height
const mouseX = e.clientX - rect.left
const mouseY = e.clientY - rect.top
const xPct = mouseX / width - 0.5
const yPct = mouseY / height - 0.5
x.set(xPct)
y.set(yPct)
}
const handleMouseLeave = () => {
x.set(0)
y.set(0)
setIsHovered(false)
}
const handleMouseEnter = () => {
setIsHovered(true)
}
return (
<motion.div
ref={ref}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onMouseEnter={handleMouseEnter}
style={{
rotateX,
rotateY,
transformStyle: "preserve-3d",
}}
className="relative w-full max-w-sm rounded-3xl bg-white dark:bg-neutral-900 shadow-xl cursor-pointer"
whileHover={{ scale: 1.02 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
{/* Spotlight Overlay */}
<div
className="absolute inset-0 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none z-10"
style={{
background: useMotionTemplate`
radial-gradient(
650px circle at ${mouseXSpring.get() * 350 + 175}px ${mouseYSpring.get() * 500 + 250}px,
rgba(255,255,255,0.1),
transparent 80%
)
`
}}
/>
{/* Image Section */}
<div className="relative h-64 w-full rounded-t-3xl overflow-hidden group">
<motion.div
style={{ transformStyle: "preserve-3d", translateZ: "50px" }}
className="absolute inset-0"
>
<img
src={product.image}
alt={product.name}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-60" />
</motion.div>
{/* Favorite Button */}
<motion.button
onClick={(e) => {
e.stopPropagation();
setIsFavorite(!isFavorite);
onToggleFavorite?.(product);
}}
style={{ translateZ: "80px" }}
className="absolute top-4 right-4 p-2.5 rounded-full bg-white/10 backdrop-blur-md border border-white/20 text-white hover:bg-white/20 transition-colors"
whileTap={{ scale: 0.9 }}
>
<Heart size={18} className={cn("transition-colors", isFavorite && "fill-red-500 text-red-500")} />
</motion.button>
{/* Rating Badge */}
<motion.div
style={{ translateZ: "60px" }}
className="absolute bottom-4 left-4 flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-black/40 backdrop-blur-md border border-white/10"
>
<Star size={14} className="fill-yellow-400 text-yellow-400" />
<span className="text-xs font-bold text-white">{product.rating}</span>
</motion.div>
</div>
{/* Content Section */}
<div className="p-6 relative z-20 bg-white dark:bg-neutral-900 rounded-b-3xl">
<motion.div style={{ translateZ: "40px" }}>
<div className="flex justify-between items-start mb-2">
<div>
<h3 className="text-xl font-bold text-neutral-800 dark:text-white tracking-tight">{product.name}</h3>
<p className="text-sm text-neutral-500 dark:text-neutral-400 font-medium">{product.category}</p>
</div>
<span className="text-lg font-bold text-neutral-900 dark:text-white">${product.price}</span>
</div>
{/* Color Selection */}
<div className="mt-6 mb-8">
<p className="text-xs font-bold text-neutral-400 uppercase tracking-wider mb-3">Select Color</p>
<div className="flex gap-3">
{product.colors.map((color) => (
<button
key={color}
onClick={() => setSelectedColor(color)}
className={cn(
"w-8 h-8 rounded-full border-2 transition-all duration-300 relative flex items-center justify-center",
selectedColor === color
? "border-neutral-900 dark:border-white scale-110"
: "border-transparent hover:scale-110"
)}
>
<span
className="w-6 h-6 rounded-full absolute"
style={{ backgroundColor: color }}
/>
</button>
))}
</div>
</div>
{/* Action Button */}
<motion.button
onClick={() => onAddToCart?.(product, selectedColor)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="w-full py-3.5 rounded-xl bg-neutral-900 dark:bg-white text-white dark:text-black font-bold text-sm shadow-lg shadow-neutral-500/20 dark:shadow-white/10 flex items-center justify-center gap-2 group/btn"
>
<ShoppingBag size={18} />
<span>Add to Cart</span>
<ArrowRight size={16} className="opacity-0 -translate-x-2 group-hover/btn:opacity-100 group-hover/btn:translate-x-0 transition-all" />
</motion.button>
</motion.div>
</div>
</motion.div>
)
}
Dependencies
framer-motion: latestlucide-react: latestclsx: latesttailwind-merge: latest

