Files
gridpilot.gg/apps/website/components/mockups/MockupStack.tsx
2026-01-19 18:01:30 +01:00

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>
);
}