180 lines
5.1 KiB
TypeScript
180 lines
5.1 KiB
TypeScript
|
|
import { motion, useReducedMotion } from 'framer-motion';
|
|
import { ReactNode, useEffect, useState } from 'react';
|
|
import { Box } from '@/ui/Box';
|
|
import { Surface } from '@/ui/Surface';
|
|
|
|
interface MockupStackProps {
|
|
children: ReactNode;
|
|
index?: number;
|
|
}
|
|
|
|
export function MockupStack({ children, index = 0 }: MockupStackProps) {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
const [isMounted, setIsMounted] = useState(false);
|
|
const [isMobile, setIsMobile] = useState(true); // Default to mobile (no animations)
|
|
|
|
useEffect(() => {
|
|
setIsMounted(true);
|
|
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
|
checkMobile();
|
|
window.addEventListener('resize', checkMobile);
|
|
return () => window.removeEventListener('resize', checkMobile);
|
|
}, []);
|
|
|
|
const seed = index * 1337;
|
|
const rotation1 = ((seed * 17) % 80 - 40) / 20;
|
|
const rotation2 = ((seed * 23) % 80 - 40) / 20;
|
|
|
|
// On mobile or before mount, render without animations
|
|
if (!isMounted || isMobile) {
|
|
return (
|
|
<Box position="relative" fullWidth fullHeight maxWidth="85vw" marginX="auto" marginY={{ base: 4, sm: 0 }} style={{ perspective: '1200px', transform: 'scale(var(--mockup-scale, 1))' }}>
|
|
<Surface
|
|
variant="muted"
|
|
position="absolute"
|
|
rounded="none"
|
|
border
|
|
borderColor="var(--ui-color-border-low)"
|
|
style={{
|
|
rotate: `${rotation1}deg`,
|
|
zIndex: 1,
|
|
top: '-8px',
|
|
left: '-8px',
|
|
right: '-8px',
|
|
bottom: '-8px',
|
|
boxShadow: '0 12px 40px rgba(0,0,0,0.3)',
|
|
opacity: 0.5,
|
|
}}
|
|
/>
|
|
|
|
<Surface
|
|
variant="muted"
|
|
position="absolute"
|
|
rounded="none"
|
|
border
|
|
borderColor="var(--ui-color-border-low)"
|
|
style={{
|
|
rotate: `${rotation2}deg`,
|
|
zIndex: 2,
|
|
top: '-4px',
|
|
left: '-4px',
|
|
right: '-4px',
|
|
bottom: '-4px',
|
|
boxShadow: '0 16px 48px rgba(0,0,0,0.35)',
|
|
opacity: 0.7,
|
|
}}
|
|
/>
|
|
|
|
<Box
|
|
position="relative"
|
|
zIndex={10}
|
|
fullWidth
|
|
fullHeight
|
|
rounded="none"
|
|
overflow="hidden"
|
|
border
|
|
borderColor="var(--ui-color-border-low)"
|
|
style={{
|
|
boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Desktop: render with animations
|
|
return (
|
|
<Box position="relative" fullWidth fullHeight maxWidth="85vw" marginX="auto" marginY={{ base: 4, sm: 0 }} style={{ perspective: '1200px', transform: 'scale(var(--mockup-scale, 1))' }}>
|
|
<motion.div
|
|
style={{
|
|
position: 'absolute',
|
|
borderRadius: '0',
|
|
backgroundColor: 'rgba(20, 22, 25, 0.8)',
|
|
border: '1px solid rgba(35, 39, 43, 0.5)',
|
|
rotate: `${rotation1}deg`,
|
|
zIndex: 1,
|
|
top: '-8px',
|
|
left: '-8px',
|
|
right: '-8px',
|
|
bottom: '-8px',
|
|
boxShadow: '0 12px 40px rgba(0,0,0,0.3)',
|
|
}}
|
|
initial={{ opacity: 0, scale: 0.92 }}
|
|
animate={{ opacity: 0.5, scale: 1 }}
|
|
transition={{ duration: 0.3, delay: 0.1 }}
|
|
/>
|
|
|
|
<motion.div
|
|
style={{
|
|
position: 'absolute',
|
|
borderRadius: '0',
|
|
backgroundColor: 'rgba(20, 22, 25, 0.9)',
|
|
border: '1px solid rgba(35, 39, 43, 0.5)',
|
|
rotate: `${rotation2}deg`,
|
|
zIndex: 2,
|
|
top: '-4px',
|
|
left: '-4px',
|
|
right: '-4px',
|
|
bottom: '-4px',
|
|
boxShadow: '0 16px 48px rgba(0,0,0,0.35)',
|
|
}}
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 0.7, scale: 1 }}
|
|
transition={{ duration: 0.3, delay: 0.15 }}
|
|
/>
|
|
|
|
<motion.div
|
|
style={{
|
|
position: 'relative',
|
|
zIndex: 10,
|
|
width: '100%',
|
|
height: '100%',
|
|
borderRadius: '0',
|
|
overflow: 'hidden',
|
|
border: '1px solid rgba(35, 39, 43, 0.3)',
|
|
boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
|
|
}}
|
|
whileHover={
|
|
shouldReduceMotion
|
|
? {}
|
|
: {
|
|
scale: 1.02,
|
|
rotateY: 3,
|
|
rotateX: -2,
|
|
y: -12,
|
|
transition: {
|
|
type: 'spring',
|
|
stiffness: 200,
|
|
damping: 20,
|
|
},
|
|
}
|
|
}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.2 }}
|
|
>
|
|
<motion.div
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
pointerEvents: 'none',
|
|
borderRadius: '0',
|
|
}}
|
|
whileHover={
|
|
shouldReduceMotion
|
|
? {}
|
|
: {
|
|
boxShadow: '0 0 40px rgba(25, 140, 255, 0.2)',
|
|
transition: { duration: 0.2 },
|
|
}
|
|
}
|
|
/>
|
|
{children}
|
|
</motion.div>
|
|
</Box>
|
|
);
|
|
}
|