import { useState } from 'react';
import { useSpring, useSprings, animated, config } from '@react-spring/web';
function AnimatedCard({ index }: { index: number }) {
const [flipped, setFlipped] = useState(false);
const { transform, opacity } = useSpring({
opacity: flipped ? 1 : 0,
transform: `perspective(600px) rotateY(${flipped ? 180 : 0}deg)`,
config: config.molasses,
});
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4'];
const icons = ['โก', '๐จ', '๐', 'โจ'];
return (
<div
style={{
position: 'relative',
width: 120,
height: 120,
cursor: 'pointer',
}}
onClick={() => setFlipped(state => !state)}
>
<animated.div
style={{
position: 'absolute',
width: '100%',
height: '100%',
borderRadius: 16,
background: colors[index],
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 40,
boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
backfaceVisibility: 'hidden',
transform,
}}
>
{icons[index]}
</animated.div>
<animated.div
style={{
position: 'absolute',
width: '100%',
height: '100%',
borderRadius: 16,
background: `linear-gradient(135deg, ${colors[index]}, ${colors[(index + 1) % 4]})`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 14,
fontWeight: 'bold',
color: 'white',
boxShadow: '0 10px 30px rgba(0,0,0,0.3)',
backfaceVisibility: 'hidden',
opacity: opacity.to(o => 1 - o),
transform: transform.to(t => `${t} rotateY(180deg)`),
}}
>
Flipped!
</animated.div>
</div>
);
}
function BouncingBalls() {
const [active, setActive] = useState(false);
const springs = useSprings(
5,
Array.from({ length: 5 }, (_, i) => ({
y: active ? -80 : 0,
scale: active ? 1.2 : 1,
delay: i * 100,
config: { tension: 300, friction: 10 },
}))
);
const colors = ['#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3', '#54a0ff'];
return (
<div
style={{
display: 'flex',
gap: 12,
cursor: 'pointer',
padding: '40px 0',
}}
onMouseEnter={() => setActive(true)}
onMouseLeave={() => setActive(false)}
>
{springs.map((spring, i) => (
<animated.div
key={i}
style={{
width: 40,
height: 40,
borderRadius: '50%',
background: colors[i],
boxShadow: '0 4px 15px rgba(0,0,0,0.2)',
transform: spring.y.to(
(y) => `translateY(${y}px) scale(${spring.scale.get()})`
),
}}
/>
))}
</div>
);
}
function NumberCounter() {
const [count, setCount] = useState(0);
const { number } = useSpring({
from: { number: 0 },
number: count,
config: { duration: 1000 },
});
return (
<div style={{ textAlign: 'center' }}>
<animated.div
style={{
fontSize: 48,
fontWeight: 'bold',
background: 'linear-gradient(135deg, #667eea, #764ba2)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
marginBottom: 16,
}}
>
{number.to(n => n.toFixed(0))}
</animated.div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
<button
onClick={() => setCount(c => c + 100)}
style={{
padding: '10px 20px',
fontSize: 16,
border: 'none',
borderRadius: 8,
background: 'linear-gradient(135deg, #667eea, #764ba2)',
color: 'white',
cursor: 'pointer',
fontWeight: 'bold',
}}
>
+100
</button>
<button
onClick={() => setCount(0)}
style={{
padding: '10px 20px',
fontSize: 16,
border: '2px solid #667eea',
borderRadius: 8,
background: 'transparent',
color: '#667eea',
cursor: 'pointer',
fontWeight: 'bold',
}}
>
Reset
</button>
</div>
</div>
);
}
function DraggableBox() {
const [{ x, y, scale }, api] = useSpring(() => ({
x: 0,
y: 0,
scale: 1,
config: { tension: 300, friction: 20 },
}));
return (
<animated.div
style={{
width: 100,
height: 100,
borderRadius: 16,
background: 'linear-gradient(135deg, #f093fb, #f5576c)',
cursor: 'grab',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontWeight: 'bold',
fontSize: 12,
boxShadow: '0 10px 30px rgba(240, 147, 251, 0.4)',
x,
y,
scale,
}}
onMouseDown={() => api.start({ scale: 0.9 })}
onMouseUp={() => api.start({ scale: 1 })}
onMouseLeave={() => api.start({ scale: 1, x: 0, y: 0 })}
onMouseMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
api.start({
x: (e.clientX - centerX) * 0.1,
y: (e.clientY - centerY) * 0.1,
});
}}
>
Hover me!
</animated.div>
);
}
export default function App() {
return (
<div
style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)',
padding: 40,
fontFamily: 'system-ui, sans-serif',
}}
>
<h1
style={{
textAlign: 'center',
color: 'white',
marginBottom: 8,
fontSize: 32,
}}
>
๐ธ React Spring Demo
</h1>
<p
style={{
textAlign: 'center',
color: 'rgba(255,255,255,0.6)',
marginBottom: 40,
}}
>
Physics-based animations for React
</p>
<div
style={{
display: 'grid',
gap: 40,
maxWidth: 600,
margin: '0 auto',
}}
>
{}
<section
style={{
background: 'rgba(255,255,255,0.05)',
borderRadius: 20,
padding: 30,
}}
>
<h2 style={{ color: 'white', marginBottom: 20, fontSize: 18 }}>
๐ด 3D Flip Cards
</h2>
<p style={{ color: 'rgba(255,255,255,0.5)', marginBottom: 20, fontSize: 14 }}>
Click cards to flip them
</p>
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', justifyContent: 'center' }}>
{[0, 1, 2, 3].map(i => (
<AnimatedCard key={i} index={i} />
))}
</div>
</section>
{}
<section
style={{
background: 'rgba(255,255,255,0.05)',
borderRadius: 20,
padding: 30,
}}
>
<h2 style={{ color: 'white', marginBottom: 20, fontSize: 18 }}>
๐พ Staggered Animation
</h2>
<p style={{ color: 'rgba(255,255,255,0.5)', marginBottom: 10, fontSize: 14 }}>
Hover over the balls
</p>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<BouncingBalls />
</div>
</section>
{}
<section
style={{
background: 'rgba(255,255,255,0.05)',
borderRadius: 20,
padding: 30,
}}
>
<h2 style={{ color: 'white', marginBottom: 20, fontSize: 18 }}>
๐ข Animated Counter
</h2>
<NumberCounter />
</section>
{}
<section
style={{
background: 'rgba(255,255,255,0.05)',
borderRadius: 20,
padding: 30,
}}
>
<h2 style={{ color: 'white', marginBottom: 20, fontSize: 18 }}>
๐ฏ Interactive Box
</h2>
<p style={{ color: 'rgba(255,255,255,0.5)', marginBottom: 20, fontSize: 14 }}>
Move your mouse over the box
</p>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<DraggableBox />
</div>
</section>
</div>
</div>
);
}