Spaces:
Running
Running
| import { useRef, useState, type ReactNode, type MouseEvent } from 'react'; | |
| import { motion } from 'framer-motion'; | |
| import { cn } from '@/lib/utils'; | |
| interface TiltCardProps { | |
| children: ReactNode; | |
| className?: string; | |
| glareColor?: string; | |
| tiltDegree?: number; | |
| } | |
| export default function TiltCard({ | |
| children, | |
| className, | |
| glareColor = 'rgba(255,255,255,0.15)', | |
| tiltDegree = 10, | |
| }: TiltCardProps) { | |
| const cardRef = useRef<HTMLDivElement>(null); | |
| const [transform, setTransform] = useState({ rotateX: 0, rotateY: 0 }); | |
| const [glare, setGlare] = useState({ x: 50, y: 50, opacity: 0 }); | |
| function handleMouseMove(e: MouseEvent<HTMLDivElement>) { | |
| if (!cardRef.current) return; | |
| const rect = cardRef.current.getBoundingClientRect(); | |
| const x = (e.clientX - rect.left) / rect.width; | |
| const y = (e.clientY - rect.top) / rect.height; | |
| setTransform({ | |
| rotateX: (0.5 - y) * tiltDegree, | |
| rotateY: (x - 0.5) * tiltDegree, | |
| }); | |
| setGlare({ x: x * 100, y: y * 100, opacity: 0.08 }); | |
| } | |
| function handleMouseLeave() { | |
| setTransform({ rotateX: 0, rotateY: 0 }); | |
| setGlare({ x: 50, y: 50, opacity: 0 }); | |
| } | |
| return ( | |
| <motion.div | |
| ref={cardRef} | |
| className={cn('relative overflow-hidden', className)} | |
| onMouseMove={handleMouseMove} | |
| onMouseLeave={handleMouseLeave} | |
| animate={{ | |
| rotateX: transform.rotateX, | |
| rotateY: transform.rotateY, | |
| }} | |
| transition={{ type: 'spring', stiffness: 300, damping: 20 }} | |
| style={{ perspective: 800, transformStyle: 'preserve-3d' }} | |
| > | |
| {children} | |
| <div | |
| className="pointer-events-none absolute inset-0 rounded-[inherit] transition-opacity duration-300" | |
| style={{ | |
| background: `radial-gradient(circle at ${glare.x}% ${glare.y}%, ${glareColor}, transparent 60%)`, | |
| opacity: glare.opacity, | |
| }} | |
| /> | |
| </motion.div> | |
| ); | |
| } | |