Spaces:
Sleeping
Sleeping
| "use client"; | |
| import clsx from "clsx"; | |
| import type { ReactNode } from "react"; | |
| import { useCallback, useRef } from "react"; | |
| import { motion } from "framer-motion"; | |
| export interface CardProps { | |
| children: ReactNode; | |
| className?: string; | |
| /** Enable spotlight hover effect */ | |
| spotlight?: boolean; | |
| /** Glass morphism intensity */ | |
| glass?: "none" | "subtle" | "medium" | "strong"; | |
| /** Depth level for layered shadows */ | |
| depth?: 0 | 1 | 2 | 3; | |
| /** Hover lift effect */ | |
| hoverLift?: boolean; | |
| /** Animated entrance */ | |
| animate?: boolean; | |
| /** Entrance animation delay */ | |
| delay?: number; | |
| } | |
| /** | |
| * Card - Translucent surface with hairline border. | |
| * Features spotlight illumination, glass morphism, and depth effects. | |
| */ | |
| export default function Card({ | |
| children, | |
| className, | |
| spotlight = true, | |
| glass = "none", | |
| depth = 1, | |
| hoverLift = true, | |
| animate = false, | |
| delay = 0, | |
| }: CardProps) { | |
| const cardRef = useRef<HTMLDivElement>(null); | |
| const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => { | |
| if (!cardRef.current || !spotlight) return; | |
| const rect = cardRef.current.getBoundingClientRect(); | |
| const x = ((e.clientX - rect.left) / rect.width) * 100; | |
| const y = ((e.clientY - rect.top) / rect.height) * 100; | |
| cardRef.current.style.setProperty("--mouse-x", `${x}%`); | |
| cardRef.current.style.setProperty("--mouse-y", `${y}%`); | |
| }, [spotlight]); | |
| const glassClasses = { | |
| none: "", | |
| subtle: "bg-surface/60 backdrop-blur-sm", | |
| medium: "bg-surface/80 backdrop-blur-md", | |
| strong: "bg-surface/95 backdrop-blur-lg", | |
| }; | |
| const depthClasses = { | |
| 0: "", | |
| 1: "depth-1", | |
| 2: "depth-2", | |
| 3: "depth-3", | |
| }; | |
| const cardContent = ( | |
| <div | |
| ref={cardRef} | |
| onMouseMove={spotlight ? handleMouseMove : undefined} | |
| className={clsx( | |
| "hairline relative overflow-hidden rounded-sm", | |
| glass === "none" && "bg-surface", | |
| glassClasses[glass], | |
| depthClasses[depth], | |
| spotlight && "spotlight-card", | |
| hoverLift && "hover-lift", | |
| className | |
| )} | |
| > | |
| {/* Inner glow at top edge */} | |
| <div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-accent/10 to-transparent" /> | |
| {/* Content */} | |
| <div className="relative z-10">{children}</div> | |
| </div> | |
| ); | |
| if (animate) { | |
| return ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 16 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ | |
| duration: 0.4, | |
| delay, | |
| ease: [0.22, 1, 0.36, 1], | |
| }} | |
| > | |
| {cardContent} | |
| </motion.div> | |
| ); | |
| } | |
| return cardContent; | |
| } | |
| /** | |
| * InteractiveCard - Card with enhanced hover/tap interactions | |
| */ | |
| export interface InteractiveCardProps extends CardProps { | |
| onClick?: () => void; | |
| href?: string; | |
| } | |
| export function InteractiveCard({ | |
| children, | |
| className, | |
| spotlight = true, | |
| glass = "none", | |
| depth = 1, | |
| hoverLift = true, | |
| onClick, | |
| href, | |
| }: InteractiveCardProps) { | |
| // Use state to track mouse position for spotlight effect | |
| const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => { | |
| if (!spotlight) return; | |
| const rect = e.currentTarget.getBoundingClientRect(); | |
| const x = ((e.clientX - rect.left) / rect.width) * 100; | |
| const y = ((e.clientY - rect.top) / rect.height) * 100; | |
| e.currentTarget.style.setProperty("--mouse-x", `${x}%`); | |
| e.currentTarget.style.setProperty("--mouse-y", `${y}%`); | |
| }, [spotlight]); | |
| const glassClasses = { | |
| none: "", | |
| subtle: "bg-surface/60 backdrop-blur-sm", | |
| medium: "bg-surface/80 backdrop-blur-md", | |
| strong: "bg-surface/95 backdrop-blur-lg", | |
| }; | |
| const depthClasses = { | |
| 0: "", | |
| 1: "depth-1", | |
| 2: "depth-2", | |
| 3: "depth-3", | |
| }; | |
| // Use motion.div for consistent typing, with conditional href on anchor | |
| return ( | |
| <motion.div | |
| onMouseMove={spotlight ? handleMouseMove : undefined} | |
| whileHover={hoverLift ? { y: -2, transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } } : undefined} | |
| whileTap={(onClick || href) ? { scale: 0.99, transition: { duration: 0.1 } } : undefined} | |
| className={clsx( | |
| "hairline group relative block w-full overflow-hidden rounded-sm text-left", | |
| glass === "none" && "bg-surface", | |
| glassClasses[glass], | |
| depthClasses[depth], | |
| spotlight && "spotlight-card", | |
| (onClick || href) && "cursor-pointer", | |
| className | |
| )} | |
| onClick={onClick} | |
| > | |
| {/* Inner glow */} | |
| <div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-accent/15 to-transparent" /> | |
| {/* Hover accent line */} | |
| <motion.div | |
| className="pointer-events-none absolute inset-x-0 bottom-0 h-px origin-left bg-gradient-to-r from-transparent via-accent/30 to-transparent" | |
| initial={{ scaleX: 0 }} | |
| whileHover={{ scaleX: 1 }} | |
| transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }} | |
| /> | |
| {/* Content */} | |
| {href ? ( | |
| <a href={href} className="relative z-10 block">{children}</a> | |
| ) : ( | |
| <div className="relative z-10">{children}</div> | |
| )} | |
| </motion.div> | |
| ); | |
| } | |