'use client' import React, { useState, useRef, useCallback, useMemo } from 'react' import { useTheme } from '@/lib/ThemeContext' import { SHADOWS, SHADOWS_DARK, MOTION, PARALLAX, MATERIALS, calculateParallax, } from '@/lib/spatial-ui/depth-system' import { usePerformance } from '@/lib/spatial-ui/PerformanceContext' /** * SpatialCard - A card component with depth-aware interactions * * Features: * - Dynamic shadows that respond to hover/active states * - Subtle parallax effect based on cursor position * - Material-based styling (glass, solid, acrylic) * - Elevation changes with physics-based motion * - Performance-adaptive (degrades gracefully) */ export type CardMaterial = 'solid' | 'glass' | 'thinGlass' | 'thickGlass' | 'acrylic' export type CardElevation = 'flat' | 'raised' | 'floating' interface SpatialCardProps { children: React.ReactNode /** Material style */ material?: CardMaterial /** Base elevation level */ elevation?: CardElevation /** Enable hover lift effect */ hoverLift?: boolean /** Enable subtle parallax on hover */ parallax?: boolean /** Parallax intensity (uses PARALLAX constants) */ parallaxIntensity?: number /** Click handler */ onClick?: () => void /** Additional CSS classes */ className?: string /** Custom padding */ padding?: 'none' | 'sm' | 'md' | 'lg' /** Border radius */ rounded?: 'none' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' /** Glow color on hover (RGB values) */ glowColor?: string /** Disable all effects */ static?: boolean } const PADDING_MAP = { none: '', sm: 'p-3', md: 'p-4', lg: 'p-6', } const ROUNDED_MAP = { none: 'rounded-none', md: 'rounded-md', lg: 'rounded-lg', xl: 'rounded-xl', '2xl': 'rounded-2xl', '3xl': 'rounded-3xl', } const ELEVATION_SHADOWS = { flat: { rest: 'none', hover: 'sm', active: 'xs' }, raised: { rest: 'sm', hover: 'lg', active: 'md' }, floating: { rest: 'lg', hover: 'xl', active: 'lg' }, } export function SpatialCard({ children, material = 'glass', elevation = 'raised', hoverLift = true, parallax = false, parallaxIntensity = PARALLAX.subtle, onClick, className = '', padding = 'md', rounded = '2xl', glowColor, static: isStatic = false, }: SpatialCardProps) { const { isDark } = useTheme() const { settings, reportAnimationStart, reportAnimationEnd, canStartAnimation } = usePerformance() const [isHovered, setIsHovered] = useState(false) const [isActive, setIsActive] = useState(false) const [parallaxOffset, setParallaxOffset] = useState({ x: 0, y: 0 }) const cardRef = useRef(null) const animatingRef = useRef(false) // Determine if effects should be enabled const effectsEnabled = !isStatic && settings.enableShadows const parallaxEnabled = parallax && settings.enableParallax && !isStatic // Shadow key type (excludes glow function) type ShadowKey = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' // Get current shadow based on state const currentShadow = useMemo((): string => { if (!effectsEnabled) return 'none' const shadows = ELEVATION_SHADOWS[elevation] const shadowKey = (isActive ? shadows.active : isHovered ? shadows.hover : shadows.rest) as ShadowKey const shadowSet = isDark ? SHADOWS_DARK : SHADOWS // Get base shadow as string (not the glow function) const baseShadow = shadowSet[shadowKey] as string // Add glow effect on hover if specified if (isHovered && glowColor && settings.enableShadows) { const glowFn = isDark ? SHADOWS_DARK.glow : SHADOWS.glow return `${baseShadow}, ${glowFn(glowColor, 0.2)}` } return baseShadow }, [effectsEnabled, elevation, isActive, isHovered, isDark, glowColor, settings.enableShadows]) // Get material styles const materialStyles = useMemo(() => { const mat = MATERIALS[material] const bg = isDark ? mat.backgroundDark : mat.background const border = isDark ? mat.borderDark : mat.border const blur = settings.enableBlur ? mat.blur * settings.blurIntensity : 0 return { background: bg, backdropFilter: blur > 0 ? `blur(${blur}px)` : 'none', WebkitBackdropFilter: blur > 0 ? `blur(${blur}px)` : 'none', borderColor: border, } }, [material, isDark, settings.enableBlur, settings.blurIntensity]) // Calculate transform based on state const transform = useMemo(() => { if (isStatic || !settings.enableSpringAnimations) { return 'translateY(0) scale(1)' } let y = 0 let scale = 1 if (isActive) { y = 1 scale = 0.98 } else if (isHovered && hoverLift) { y = -3 scale = 1.01 } // Add parallax offset const px = parallaxEnabled ? parallaxOffset.x : 0 const py = parallaxEnabled ? parallaxOffset.y : 0 return `translateY(${y}px) translateX(${px}px) translateZ(${py}px) scale(${scale})` }, [isStatic, settings.enableSpringAnimations, isActive, isHovered, hoverLift, parallaxEnabled, parallaxOffset]) // Get transition timing const transitionDuration = useMemo(() => { const base = isActive ? MOTION.micro.duration : MOTION.standard.duration return Math.round(base * settings.animationSpeed) }, [isActive, settings.animationSpeed]) // Handlers const handleMouseEnter = useCallback(() => { if (isStatic) return if (canStartAnimation() && !animatingRef.current) { animatingRef.current = true reportAnimationStart() } setIsHovered(true) }, [isStatic, canStartAnimation, reportAnimationStart]) const handleMouseLeave = useCallback(() => { setIsHovered(false) setParallaxOffset({ x: 0, y: 0 }) if (animatingRef.current) { animatingRef.current = false reportAnimationEnd() } }, [reportAnimationEnd]) const handleMouseMove = useCallback( (e: React.MouseEvent) => { if (!parallaxEnabled || !cardRef.current) return const rect = cardRef.current.getBoundingClientRect() const offset = calculateParallax( e.clientX, e.clientY, rect, parallaxIntensity * settings.parallaxIntensity ) setParallaxOffset(offset) }, [parallaxEnabled, parallaxIntensity, settings.parallaxIntensity] ) const handleMouseDown = useCallback(() => { if (!isStatic) setIsActive(true) }, [isStatic]) const handleMouseUp = useCallback(() => { setIsActive(false) }, []) const handleClick = useCallback(() => { onClick?.() }, [onClick]) return (
{children}
) } /** * SpatialCardHeader - Header section for SpatialCard */ export function SpatialCardHeader({ children, className = '', }: { children: React.ReactNode className?: string }) { const { isDark } = useTheme() return (
{children}
) } /** * SpatialCardContent - Main content area */ export function SpatialCardContent({ children, className = '', }: { children: React.ReactNode className?: string }) { return
{children}
} /** * SpatialCardFooter - Footer section */ export function SpatialCardFooter({ children, className = '', }: { children: React.ReactNode className?: string }) { const { isDark } = useTheme() return (
{children}
) }