standard analysis · bottom-sheet · stagger · spring
A multi-step wallet removal flow featuring a bottom sheet dismissal, staggered checkbox se
A multi-step wallet removal flow featuring a bottom sheet dismissal, staggered checkbox selections, a Dynamic Island FaceID authentication, and a vertical-to-horizontal collapse animation for the wallet card.
The process begins with an 'Options' sheet sliding down to reveal the main settings. Navigating into the removal flow triggers a transition to a checklist where items are marked with a snappy red fill. Authentication is confirmed via a Dynamic Island expansion before the wallet card undergoes a dramatic destruction sequence—collapsing into a horizontal line and then vanishing—leading to a clean success state.
Choreography, beat by beat
- Options sheet slides down + background de-blurs
- Screen transitions to 'Before you continue' checklist
- Four checkboxes are tapped in sequence (trace-in + fill)
- FaceID authentication expands from Dynamic Island
- Red wallet card appears + status spinner rotates
- Wallet card collapses vertically into a thin red line
- Red line shrinks horizontally to a center point and vanishes
- Final success screen fades in
| Element | Change | From → To | Timing |
|---|---|---|---|
| Options Bottom Sheet | transform | y:0 → y:400px | 400ms · ease-in |
| Background Scrim | reveal | blur(20px) → blur(0px) | 400ms · ease-out |
| Checkboxes | transform | 0.9 → 1.0 | 200ms · ease-out-back |
| Dynamic Island | morph | capsule → rounded square | 500ms · ease-out-back |
| Wallet Card | transform | height:200px → height:1px | 300ms · ease-in-out |
| Wallet Card | transform | width:340px → width:0px | 200ms · ease-in |
| Success Text | reveal | 0.0 → 1.0 | 500ms · ease-out |
What video honestly can't tell you
- exact spring constants for the FaceID expansion
- precise duration of the checklist screen navigation push
- off-screen state of the main wallet list during removal animation
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence, useAnimation } from "framer-motion";
/**
* MotionLens Code Compiler
* REPRODUCTION: Wallet Removal Flow
* STATUS: Corrected Audit
*/
const RED = "#FF2D55";
const RED_DISABLED = "#FFB2BE";
const BG_WHITE = "#FFFFFF";
const TEXT_MAIN = "#1C1C1E";
const TEXT_SUB = "#8E8E93";
const EASE_OUT_BACK = [0.34, 1.56, 0.64, 1];
const EASE_IN_OUT = [0.42, 0, 0.58, 1];
const EASE_IN = [0.42, 0, 1, 1];
const EASE_OUT = [0, 0, 0.58, 1];
export default function WalletRemovalFlow() {
const [step, setStep] = useState(0);
const [checkedCount, setCheckedCount] = useState(0);
// Timeline Controller based on Blueprint
useEffect(() => {
const timers = [
setTimeout(() => setStep(1), 100), // 0ms: Options sheet begins sliding down
setTimeout(() => setStep(2), 333), // 333ms: Checklist screen slides in
setTimeout(() => setStep(3), 3500), // 3500ms: Checkboxes begin
setTimeout(() => setStep(4), 6000), // 6000ms: Dynamic Island FaceID
setTimeout(() => setStep(5), 7000), // 7000ms: Red Wallet card appears
setTimeout(() => setStep(6), 8667), // 8667ms: Vertical collapse
setTimeout(() => setStep(7), 9167), // 9167ms: Horizontal shrink
setTimeout(() => setStep(8), 9333), // 9333ms: Success screen
];
return () => timers.forEach(clearTimeout);
}, []);
// Checkbox auto-fill sequence (Stagger 400ms)
useEffect(() => {
if (step === 3) {
const interval = setInterval(() => {
setCheckedCount((prev) => (prev < 4 ? prev + 1 : prev));
}, 400);
return () => clearInterval(interval);
}
}, [step]);
return (
<div style={{
width: "100%",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#F2F2F7",
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
WebkitFontSmoothing: "antialiased"
}}>
<div style={{
width: 393,
height: 852,
backgroundColor: BG_WHITE,
borderRadius: 54,
position: "relative",
overflow: "hidden",
boxShadow: "0 20px 50px rgba(0,0,0,0.15)",
border: "12px solid #1c1c1e"
}}>
{/* Status Bar */}
<div style={{
padding: "18px 32px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
fontSize: 15,
fontWeight: 600,
position: "relative",
zIndex: 101
}}>
<span>9:41</span>
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
<svg width="18" height="12" viewBox="0 0 26 13" fill="black"><path d="M0 3C0 1.34315 1.34315 0 3 0H21C22.6569 0 24 1.34315 24 3V10C24 11.6569 22.6569 13 21 13H3C1.34315 13 0 11.6569 0 10V3Z" fillOpacity="0.3"/><path d="M2 3C2 1.89543 2.89543 1 4 1H19C20.1046 1 21 1.89543 21 3V10C21 11.1046 20.1046 12 19 12H4C2.89543 12 2 11.1046 2 10V3Z" fill="black"/></svg>
</div>
</div>
{/* Dynamic Island - Morphing Component */}
<div style={{
position: "absolute",
top: 11,
left: "50%",
transform: "translateX(-50%)",
zIndex: 1000
}}>
<motion.div
animate={{
width: step === 4 ? 160 : 124,
height: step === 4 ? 160 : 37,
borderRadius: step === 4 ? 44 : 20,
y: step === 4 ? 10 : 0
}}
transition={{ duration: 0.5, ease: EASE_OUT_BACK }}
style={{
backgroundColor: "black",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
color: "white",
overflow: "hidden"
}}
>
<AnimatePresence>
{step === 4 && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12 }}
>
<div style={{
width: 64,
height: 64,
borderRadius: "50%",
border: "3px solid #34C759",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}>
<motion.svg
width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#34C759" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.4, delay: 0.2 }}
>
<polyline points="20 6 9 17 4 12" />
</motion.svg>
</div>
<span style={{ fontSize: 13, fontWeight: 600, letterSpacing: -0.2 }}>FaceID</span>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</div>
{/* Background Scrim (De-blurring) */}
<motion.div
animate={{ backdropFilter: step >= 1 ? "blur(0px)" : "blur(20px)" }}
transition={{ duration: 0.4, ease: EASE_OUT }}
style={{ position: "absolute", inset: 0, zIndex: 5, pointerEvents: "none" }}
/>
{/* SCREEN 1: Remove Wallet Main */}
<AnimatePresence>
{step <= 1 && (
<motion.div
exit={{ opacity: 0 }}
style={{ padding: "0 24px", position: "absolute", inset: 0, top: 100 }}
>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 32 }}>
<span style={{ fontSize: 22, color: TEXT_SUB }}>✕</span>
<span style={{ fontSize: 22, color: TEXT_SUB }}>ⓘ</span>
</div>
<h1 style={{ fontSize: 32, fontWeight: 700, margin: "0 0 12px 0", letterSpacing: -0.5 }}>Remove Wallet</h1>
<p style={{ color: TEXT_SUB, fontSize: 17, marginBottom: 48, lineHeight: 1.3 }}>
This wallet will be permanently removed from your device.
</p>
{/* Card Preview (Frame 1 Fidelity) */}
<div style={{
width: 345, height: 210, backgroundColor: RED, borderRadius: 28, padding: 24, color: "white",
position: "relative", margin: "0 auto", boxShadow: "0 15px 35px rgba(255,45,85,0.25)"
}}>
<div style={{ fontSize: 44 }}>😇</div>
<div style={{ position: "absolute", top: 24, right: 24, fontSize: 15, fontWeight: 500, opacity: 0.9 }}>
0xa991...5bc4 <span style={{ marginLeft: 4 }}>❐</span>
</div>
<div style={{ position: "absolute", bottom: 24, left: 24 }}>
<div style={{ fontWeight: 700, fontSize: 20, marginBottom: 2 }}>60fps</div>
<div style={{ fontSize: 15, opacity: 0.8 }}>0 ETH</div>
</div>
<div style={{
position: "absolute", bottom: 24, right: 24,
backgroundColor: "rgba(255,255,255,0.25)",
padding: "6px 14px", borderRadius: 20, fontSize: 13, fontWeight: 600,
display: "flex", alignItems: "center", gap: 4
}}>
<span style={{ fontSize: 10 }}>✓</span> Backed Up
</div>
</div>
{/* Options Bottom Sheet (Dismissal) */}
<AnimatePresence>
{step === 0 && (
<motion.div
initial={{ y: 0 }}
exit={{ y: 400 }}
transition={{ duration: 0.4, ease: EASE_IN }}
style={{
position: "absolute", bottom: -100, left: 0, right: 0, height: 340,
backgroundColor: "white", borderTopLeftRadius: 32, borderTopRightRadius: 32,
boxShadow: "0 -10px 40px rgba(0,0,0,0.12)", padding: 24, zIndex: 60
}}
>
<div style={{ width: 36, height: 5, backgroundColor: "#E5E5EA", borderRadius: 10, margin: "0 auto 32px" }} />
<div style={{ height: 56, backgroundColor: RED, borderRadius: 18, display: "flex", alignItems: "center", justifyContent: "center", color: "white", fontWeight: 600, fontSize: 17 }}>
Continue
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
{/* SCREEN 2: Checklist (Frame 2 Fidelity) */}
<AnimatePresence>
{step >= 2 && step < 5 && (
<motion.div
initial={{ x: 393 }}
animate={{ x: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
style={{ padding: "0 24px", position: "absolute", inset: 0, top: 100, backgroundColor: BG_WHITE, zIndex: 10 }}
>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 32 }}>
<span style={{ fontSize: 24, color: TEXT_SUB }}>‹</span>
<div style={{ backgroundColor: "#FFE5E9", padding: "6px 10px", borderRadius: 8, fontSize: 14 }}>⚠️</div>
<span style={{ fontSize: 24, color: TEXT_SUB }}>ⓘ</span>
</div>
<h1 style={{ fontSize: 28, fontWeight: 700, margin: "0 0 12px 0", letterSpacing: -0.5 }}>Before you continue...</h1>
<p style={{ color: TEXT_SUB, fontSize: 16, marginBottom: 40, lineHeight: 1.4 }}>
Please read and agree to the following terms before you continue.
</p>
<div style={{ display: "flex", flexDirection: "column", gap: 28 }}>
{[
"I understand that removing my wallet does not delete it—but simply removes it from my device.",
"I understand that if I haven't backed up my wallet before removing it, I may lose access to it.",
"I understand that if I ever lose access to my wallets, Family is not liable and cannot help in any way.",
"I understand that removing my wallet is a risky action that can have unintended consequences."
].map((text, i) => (
<div key={i} style={{ display: "flex", gap: 18, alignItems: "flex-start" }}>
<motion.div
animate={{
backgroundColor: checkedCount > i ? RED : "transparent",
scale: checkedCount === i + 1 ? [0.9, 1.1, 1] : 1,
borderColor: checkedCount > i ? RED : "#E5E5EA"
}}
transition={{ duration: 0.2, ease: EASE_OUT_BACK }}
style={{
minWidth: 26, height: 26, borderRadius: 7, border: "2px solid",
display: "flex", alignItems: "center", justifyContent: "center"
}}
>
<AnimatePresence>
{checkedCount > i && (
<motion.svg
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</motion.svg>
)}
</AnimatePresence>
</motion.div>
<p style={{ fontSize: 14, color: TEXT_MAIN, margin: 0, lineHeight: 1.4, fontWeight: 400 }}>{text}</p>
</div>
))}
</div>
<motion.div
animate={{
backgroundColor: checkedCount === 4 ? RED : RED_DISABLED,
scale: checkedCount === 4 ? 1 : 0.98
}}
style={{
position: "absolute", bottom: 60, left: 24, right: 24, height: 58,
borderRadius: 30, display: "flex", alignItems: "center", justifyContent: "center",
color: "white", fontWeight: 600, gap: 10, fontSize: 17
}}
>
<div style={{ border: "1.5px solid white", borderRadius: 6, padding: 2, display: "flex" }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2"/></svg>
</div>
Continue to Remove
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* SCREEN 3: Destruction Sequence (Climax) */}
<AnimatePresence>
{step >= 5 && step < 8 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: BG_WHITE, zIndex: 20 }}
>
<motion.div
initial={{ width: 345, height: 210, borderRadius: 28 }}
animate={{
height: step >= 6 ? 1 : 210,
width: step >= 7 ? 0 : 345,
borderRadius: step >= 6 ? 0 : 28,
opacity: step >= 7 ? 0 : 1
}}
transition={{
height: { duration: 0.3, ease: EASE_IN_OUT },
width: { duration: 0.2, ease: EASE_IN }
}}
style={{
backgroundColor: RED,
boxShadow: step >= 6 ? "none" : "0 20px 40px rgba(255,45,85,0.3)"
}}
/>
</motion.div>
)}
</AnimatePresence>
{/* SCREEN 4: Success Screen */}
<AnimatePresence>
{step === 8 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, ease: EASE_OUT }}
style={{
position: "absolute", inset: 0, display: "flex", flexDirection: "column",
alignItems: "center", justifyContent: "center", padding: 32, backgroundColor: BG_WHITE, zIndex: 30
}}
>
<motion.div
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: "spring", damping: 12, stiffness: 200 }}
style={{
width: 88, height: 88, backgroundColor: "#34C759", borderRadius: "50%",
display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 32,
boxShadow: "0 10px 25px rgba(52,199,89,0.3)"
}}
>
<svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
</motion.div>
<h2 style={{ fontSize: 28, fontWeight: 700, marginBottom: 12, letterSpacing: -0.5 }}>Wallet Removed</h2>
<p style={{ color: TEXT_SUB, textAlign: "center", marginBottom: 60, fontSize: 17, lineHeight: 1.4 }}>
Your wallet has been successfully removed from this device.
</p>
<motion.div
whileTap={{ scale: 0.96 }}
onClick={() => window.location.reload()}
style={{
width: "100%", height: 58, borderRadius: 30, backgroundColor: "#F2F2F7",
display: "flex", alignItems: "center", justifyContent: "center",
color: TEXT_MAIN, fontWeight: 600, fontSize: 17, cursor: "pointer"
}}
>
Done
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* Home Indicator */}
<div style={{
positionRecreate it yourself
This entire page was generated by one analysis. Point your AI agent at any clip or live site and get the same blueprint — plus ready-to-paste code. Try MotionLens free (5 credits, no card) or read the Claude Code setup guide.