birthday / src /components /OpeningAnimation.tsx
teganmosi
Add Docker deployment config and application source code
3119726
Raw
History Blame Contribute Delete
11.9 kB
import React, { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { Heart, Sparkles, Gift, HeartHandshake, Eye, ArrowRight } from "lucide-react";
interface OpeningAnimationProps {
gfNickname: string;
bfName: string;
customLetter: string;
onOpenComplete: () => void;
}
export default function OpeningAnimation({
gfNickname,
bfName,
customLetter,
onOpenComplete,
}: OpeningAnimationProps) {
const [stage, setStage] = useState<"heybaby" | "envelope" | "letter">("heybaby");
// Stabilize little floating hearts inside the view with useMemo to prevent lag/re-eval on every render.
const particles = React.useMemo(() => {
return Array.from({ length: 8 }, (_, i) => ({
id: i,
left: `${(i * 12) + 8}%`, // evenly staggered distribution across the screen
delay: i * 0.6,
size: (i % 3 === 0) ? 14 : (i % 3 === 1) ? 18 : 22,
duration: 12 + (i % 3) * 3,
}));
}, []);
// Clean the letter message into paragraphs
const letterParagraphs = customLetter.split("\n\n");
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-radial from-[#ffe4e6] via-[#fff1f2] to-[#ffe4e6] overflow-hidden px-4 select-none">
{/* Decorative Floating Hearts Backdrop */}
<div className="absolute inset-0 pointer-events-none opacity-40">
{particles.map((p) => (
<motion.div
key={p.id}
className="absolute text-rose-300"
style={{
left: p.left,
bottom: "-10%",
fontSize: p.size,
willChange: "transform",
}}
animate={{
y: "-120vh",
x: p.id % 2 === 0 ? 15 : -15,
rotate: p.id % 2 === 0 ? 45 : -45,
}}
transition={{
duration: p.duration,
repeat: Infinity,
delay: p.delay,
ease: "linear",
}}
>
❤️
</motion.div>
))}
</div>
<AnimatePresence mode="wait">
{/* Stage 0: The Sweet Greeting 'hey baby...' */}
{stage === "heybaby" && (
<motion.div
key="heybaby-stage"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, y: -40, scale: 1.05 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center max-w-md w-full text-center px-4"
>
{/* Pulsating central love anchor */}
<motion.div
animate={{ scale: [1, 1.15, 1], rotate: [0, 5, -5, 0] }}
transition={{ repeat: Infinity, duration: 2, ease: "easeInOut" }}
className="text-rose-500 mb-8"
>
<Heart className="w-16 h-16 fill-rose-500 stroke-rose-400 drop-shadow-md" />
</motion.div>
{/* Title requested by client */}
<motion.h1
initial={{ y: 20 }}
animate={{ y: 0 }}
transition={{ delay: 0.3, type: "spring" }}
className="font-script text-7xl md:text-8xl text-rose-600 block leading-tight py-2"
>
hey baby...
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.7, duration: 0.8 }}
className="text-slate-600 font-display text-base md:text-lg font-medium mt-4 tracking-wide max-w-sm"
>
It's your special day, and i built something sweet just for you
</motion.p>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.3, duration: 0.6 }}
className="mt-10"
>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.96 }}
onClick={() => setStage("envelope")}
className="bg-white hover:bg-rose-50 text-rose-600 font-display font-semibold text-sm md:text-base px-8 py-4 rounded-full shadow-lg border border-rose-100 flex items-center gap-2.5 cursor-pointer animate-heartbeat"
id="view-gift-button"
>
<span>Tap to see my surprise</span>
<Sparkles className="w-4 h-4 text-rose-500" />
</motion.button>
</motion.div>
</motion.div>
)}
{/* Stage 1: Closed Envelope / Pulsing Gift Card */}
{stage === "envelope" && (
<motion.div
key="envelope-stage"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 1.05, opacity: 0 }}
transition={{ type: "spring", stiffness: 120, damping: 16 }}
className="flex flex-col items-center max-w-sm w-full text-center"
style={{ willChange: "transform" }}
>
<div className="mb-6 relative">
<motion.div
animate={{ rotate: [-2, 2, -2] }}
transition={{ repeat: Infinity, duration: 5, ease: "easeInOut" }}
className="bg-white/80 backdrop-blur-md px-6 py-2.5 rounded-full shadow-sm border border-rose-100 text-rose-500 font-display font-medium text-sm inline-flex items-center gap-1.5"
style={{ willChange: "transform" }}
>
<Sparkles className="w-4 h-4 text-rose-400" />
I prepared a letter with love!
</motion.div>
</div>
{/* Interactive Envelope Graphic */}
<motion.div
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => setStage("letter")}
className="relative w-80 h-52 bg-gradient-to-br from-rose-400 to-rose-500 rounded-2xl shadow-xl hover:shadow-2xl cursor-pointer flex items-center justify-center border-t-2 border-rose-300 overflow-hidden"
style={{ willChange: "transform" }}
id="cute-envelope-button"
>
{/* Envelope flap folds and layers */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-rose-300/40 via-transparent to-transparent pointer-events-none" />
{/* Diagonal back lines mimicking envelope flap */}
<svg className="absolute inset-0 w-full h-full pointer-events-none" viewBox="0 0 320 208" fill="none">
<path d="M0 0 L160 110 L320 0" stroke="#f43f5e" strokeWidth="2" opacity="0.4" />
<path d="M0 208 L140 100" stroke="#f43f5e" strokeWidth="1.5" opacity="0.3" />
<path d="M320 208 L180 100" stroke="#f43f5e" strokeWidth="1.5" opacity="0.3" />
</svg>
{/* Glowing pulsating Wax Seal */}
<div className="absolute flex flex-col items-center justify-center">
<motion.div
className="w-16 h-16 rounded-full bg-amber-400 flex items-center justify-center shadow-lg border-2 border-amber-300 text-amber-900 animate-heartbeat relative"
>
<Heart className="w-8 h-8 fill-rose-600 text-rose-600" />
<span className="absolute inset-0 rounded-full border-2 border-dotted border-amber-100/60 scale-90" />
</motion.div>
<p className="mt-3 text-white text-xs font-display tracking-widest uppercase font-semibold">
Tap to open seal
</p>
</div>
</motion.div>
<p className="mt-6 text-rose-600/80 font-display tracking-wider text-sm">
Sent with absolute adoration by {bfName}
</p>
</motion.div>
)}
{/* Stage 2: Revealing the Love Letter */}
{stage === "letter" && (
<motion.div
key="letter-stage"
initial={{ y: 200, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="w-full max-w-2xl bg-[#fffdfe] rounded-3xl shadow-2xl border border-rose-100 p-6 md:p-10 relative overflow-hidden flex flex-col max-h-[90vh]"
>
{/* Top golden crest */}
<div className="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-rose-300 via-pink-400 to-rose-300" />
{/* Corner floral motifs using shapes */}
<div className="absolute -top-8 -right-8 w-24 h-24 bg-rose-100/40 rounded-full pointer-events-none" />
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-rose-100/40 rounded-full pointer-events-none" />
{/* Letter Header */}
<div className="text-center mb-6">
<motion.div
initial={{ scale: 0.5 }}
animate={{ scale: 1, rotate: [0, -10, 10, 0] }}
transition={{ duration: 1.2, delay: 0.2 }}
className="w-12 h-12 bg-rose-50 rounded-full flex items-center justify-center mx-auto text-rose-500 border border-rose-100"
>
<Gift className="w-6 h-6" />
</motion.div>
<h2 className="mt-3 font-script text-4xl text-rose-600 font-medium">
Dearest {gfNickname},
</h2>
</div>
{/* Letter Content Pane (Scrollable within paper) */}
<div className="flex-1 overflow-y-auto pr-2 overflow-x-hidden space-y-4 max-h-[50vh] text-slate-700 font-display text-sm md:text-base leading-relaxed text-center sm:text-left">
{letterParagraphs.map((para, idx) => (
<motion.p
key={idx}
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 + idx * 0.2 }}
className="whitespace-pre-line last:font-semibold"
>
{para}
</motion.p>
))}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 + letterParagraphs.length * 0.2 }}
className="pt-6 border-t border-rose-50/80 text-right font-script text-3xl text-rose-500"
>
For you, with love,
<div className="font-display text-slate-800 font-semibold text-lg pr-4 mt-1">
— {bfName}
</div>
</motion.div>
</div>
{/* Love Footer Navigation To Timeline */}
<div className="mt-6 pt-4 border-t border-rose-100/80 text-center flex flex-col items-center">
<motion.button
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.98 }}
onClick={onOpenComplete}
className="bg-gradient-to-r from-rose-500 to-pink-600 text-white rounded-full px-8 py-3.5 shadow-lg shadow-rose-500/20 hover:shadow-rose-500/30 font-display font-medium text-sm md:text-base flex items-center gap-2.5 cursor-pointer hover:from-rose-600 hover:to-pink-700 transition-colors animate-heartbeat"
id="enter-universe-button"
>
<span>Take a Walk Down Memory Lane</span>
<HeartHandshake className="w-5 h-5 fill-white" />
</motion.button>
<p className="mt-3 text-slate-400 text-xs font-display flex items-center gap-1">
<Eye className="w-3.5 h-3.5" />
Scroll down & explore interactive widgets next
</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}