Animation Design Plan — Deck-Builder blueprint-morph + card-flight

Stage 2 review alignment page for /animation-design-planner. Planning run only: design/review artifacts, no implementation code, no new dependencies.

alignment_status: review prototype tier
generated: 2026-06-10
amended: 2026-06-10 — added directional motion demos to section 4 (user request); still in review. Prior page archived to docs/history/archive/2026-06-10/173258/alignment/
skill: animation-design-planner (motion-framer subskill — framer-motion 12.40 detected)
working packet: research/skills-showcase/_working/preliminary-animation-design-planner-research.md (non-canonical)
canonical destination: apps/skills-showcase/docs/animation-plan-deck-builder.md — written only after approval
upstream: apps/skills-showcase/docs/deck-builder-ux.md (commit 6e2e6aec)

1. Context & Evidence

Section feedback:

The deck-builder UX design (apps/skills-showcase/docs/deck-builder-ux.md, committed 6e2e6aec) introduces two new framer-motion transitions that don't exist in the /prototype flow:

/animation-design-planner was invoked to plan these before building — naming the visible motion contracts, lifecycle owner, state phases, and proof method so implementation doesn't start with local timing tweaks. Framework detected: framer-motion 12.40 (imported as "framer-motion") → motion-framer subskill guardrails apply.

Evidence behind the load-bearing decisions

Claim / decisionEvidence (observed)InferenceConfidence
Both transitions are new compositions, not new primitivesdocs/deck-builder-ux.md design principle 3 ("Promote, don't rewrite"); existing primitives in SealedPack.tsx, PackOpener.tsx, SkillCard.tsx, BottomSheetThe plan reuses layoutId morph + collapse-to-target motion; no new animation library surface neededHigh
layoutId + declarative animate on transforms conflictPackOpener.tsx:212-213 — existing workaround in the pack flowFlight clones must be driven imperatively with motion values; slot cards (declarative pulses) never carry a layoutIdHigh (repo-observed)
Route-first close would break the morph-backApp Router route-change unmounts are not AnimatePresence-aware (framework behavior)Client-state-first with shallow pushState is required; real /deck/[slug] route kept for hard loads/SEOMedium — spike required on Next 16.2.6 (see risk register)
Sheet children cannot paint above the slot stripBottomSheet's z-50 stacking context in the prototype flowCard-flight needs a portal clone overlay (FlightLayer) rather than unclipping the sheetHigh

2. Motion Contract A — blueprint-morph

Section feedback:
TriggerTap/click a deck blueprint on the table (/); close via Back button or dismiss affordance.
Start → endSmall blueprint card on the table → full builder panel (/deck/[slug]) with phase slots; reverse on close. URL changes both ways.
Focal / continuity elementThe blueprint card chrome is the single visually continuous rectangle (shared layoutId="deck-blueprint-${slug}"); builder content (slot columns, header, shelf glow) staggers in after the morph lands.

Never list

Interruption rules

Gate: blueprint-morph contract acceptance

Do you accept the blueprint-morph visible motion contract (trigger, continuity element, never list, interruption rules) as the binding spec for implementation?

3. Motion Contract B — card-flight

Section feedback:
TriggerTap an uncollected card in the open fan drawer; "Collect all N" triggers the staggered batch variant.
Start → endCard visually leaves the fan, flies above the sheet to its phase slot, slot fills and pulses once, deck counter ticks; the fan card is dimmed with an "in deck ♦" badge from the tap frame onward.
Focal elementThe flying clone; the slot is the continuity target (pinned visible above the sheet on mobile).

Never list

Interruption rules

Gate: card-flight contract acceptance

Do you accept the card-flight visible motion contract (trigger, focal element, never list, interruption rules), including the add-all stagger variant, as the binding spec for implementation?

4. Interactive: Motion Demos & Phase Timeline Scrubbers

Section feedback:
Amended 2026-06-10: two directional motion demos added at user request, ahead of the timeline scrubbers. They mock the intended feel of each transition with plain CSS/JS — real implementation uses framer-motion layout springs, so easing and physics here are approximations of direction, sequencing, and continuity, not final tuning.

⚠ These are ephemeral previews, not production code. The demos show directional motion; the scrubbers show the phase timing data. All timings are design targets that pass through dbg.scaleT at implementation time. No state persists.

Directional demo — blueprint-morph

Tap the Devtool AFPS blueprint (or press Open). The chrome rectangle is the only continuously visible element; the blueprint sits at opacity 0 underneath while the panel owns the morph, and builder content staggers in only after the morph lands. Close runs the reverse: content vanishes near-instantly so the morph-back reads.

DECKS (pick a blueprint)
VARDviral RD · ▸ 3 cards
Business AFPSdeliberate · ▸ 35 cards
THE SHELF  ▓vard▓ ▓ord▓ ▓devtool▓ ▓business-discovery▓ ▓game▓
DeckFlowPhase: table — tap the Devtool AFPS blueprint or press Open.

Directional demo — card-flight

Tap any card in the fan: it dims with an "in deck ♦" badge the same frame (optimistic commit), a clone flies above the sheet to its slot, the slot fills and pulses, the counter ticks. Collect all staggers launches 70 ms apart. Dismiss mid-flight demos snap-to-end: clones vanish, slots force-fill, the counter reconciles to the committed size. Re-tapping a collected card is a no-op.

0/5 collected
Position
Adopt
Journey
Docs
Monetize
PACK: DEVTOOL (fan drawer)
FlightRecords: none — tap a card in the fan, or press Collect all.

blueprint-morph timeline

SequencePhaseWindow (ms)What happens
OpenTap commit0–20pushState; blueprint claims opacity 0; panel claims layoutId
OpenLayout spring morph20–470~450 ms framer layout spring; chrome rectangle morphs table card → panel
OpenPhase flip470–490onLayoutAnimationComplete (one-shot ref) → builder-open
OpenContent stagger-in490–690Slot columns then shelf, ~200 ms total, 40 ms stagger
CloseContent fade-out0–10Near-instant (0.01 s) so the morph reads
CloselayoutId handback10–30AnimatePresence onExitComplete → blueprint reclaims id
CloseMorph-back (apex)30–380~350 ms; completion frame is the flash-risk apex
CloseTable restored380–400Phase → table; focus returns to originating blueprint

card-flight timeline

SequencePhaseWindow (ms)What happens
Single tapTap commit0–16Optimistic commit (collectedCardIds + localStorage); fan card dims with "in deck ♦" badge, same frame
Single tapMeasure16–48Measure source rect; if slot off-screen, scrollIntoView({behavior:"instant"}) then re-measure next rAF
Single tapClone flight48–448Clone mounts in FlightLayer at source rect; imperative animate() x/y/scale, spring {stiffness: 260, damping: 26} (~400 ms)
Single tapLand + pulse448–698.then(): clone removed, slot ghost→filled, declarative pulse scale [1, 1.08, 1] ~250 ms, counter ticks
Add-allStaggered launches0–840Filtered queue, 70 ms stagger per flight (13-card worst case ≈ 0.84 s launch tail)
Add-allLast clone in flight840–1240Concurrent flights overlap; each follows the single-tap spring
Add-allLast land + batch-complete1240–1490Final pulse; batch-complete via decrement ref; single tap is a batch of 1

5. Lifecycle Ownership Map

Section feedback:

Hierarchical composition; the existing PackFlowPhase machine is nested unchanged:

DeckTableShell (client; rendered by both app/page.tsx and app/deck/[slug]/page.tsx)
├── owns DeckFlowPhase: table → blueprint-morphing → builder-open → builder-dismissing → table
├── owns activeDeck + collectedCardIds (localStorage-backed, per slug)
├── one <LayoutGroup> wrapping table + builder
├── TableSurface (always mounted)
├── BuilderPanel (AnimatePresence; mounted while activeDeck != null)
│   └── BuilderPackFlow — PackFlowPhase owner extracted from PrototypeInner
│       (sealed → opening-apex → drawer-open → closing-* chain, verbatim)
│       └── owns flights Map<cardId, FlightRecord> (parallel records, NOT a scalar phase:
│           queued → measuring → in-flight → settling → done per record)
└── FlightLayer (fixed inset-0, pointer-events-none, z-[70]) — flight clones
Routing decision (load-bearing): the morph is client-state-first; the URL syncs via shallow window.history.pushState (+ popstate listener), because App Router route-change unmounts are not AnimatePresence-aware — a route-first intercepting-route close would unmount the builder before the blueprint can reclaim the layoutId. /deck/[slug] exists as a real route for hard loads/SEO and renders the same shell with initialDeckSlug.
Spike first (~30 min): confirm shallow pushState + usePathname/popstate behavior on Next 16.2.6 — flagged as the top pre-implementation risk.

Identity rules

Commit timing

OptimisticcollectedCardIds + localStorage commit at tap time; animation is pure presentation; a presentation-level displayedCount ticks on flight-land and reconciles on any interrupt. No rollback paths exist.

Interruption rule

finishAllFlightsImmediately() (snap-to-end: stop controls, remove clones, force slots filled, reconcile counter) runs before any drawer close or builder dismiss. Back/dismiss with the drawer open uses a fast drawer teardown (BottomSheet's existing 0.01s fadeExit path, skip the collapse ritual) so leaving feels immediate.

Gate: lifecycle ownership & routing

Do you approve the hierarchical ownership map (DeckTableShell owns DeckFlowPhase; the existing PackFlowPhase machine nests unchanged) and the client-state-first routing decision (shallow pushState, with the Next 16.2.6 spike run before implementation)?

Gate: interruption semantics & commit timing

Do you approve snap-to-end interruption handling (finishAllFlightsImmediately() before any close/dismiss; fast drawer teardown on back) and the optimistic commit model (state + localStorage commit at tap time, no rollback paths)?

6. Interactive: State Machine Explorer

Section feedback:

⚠ This is an ephemeral preview, not production code. Click a state node to highlight its legal transitions and guards. The DeckFlowPhase chain is the new shell machine; the FlightRecord chain is per-card (a Map of parallel records, not a scalar phase). No state persists.

Click a state node to inspect its transitions.
MachineFromToTrigger / guard
DeckFlowPhasetableblueprint-morphingBlueprint tap — taps ignored unless phase is table
DeckFlowPhaseblueprint-morphingbuilder-openonLayoutAnimationComplete (one-shot ref)
DeckFlowPhaseblueprint-morphingbuilder-dismissingDismiss-during-morph — retargets the layout morph from its current projected position
DeckFlowPhasebuilder-openbuilder-dismissingBack/dismiss (only legal here); runs finishAllFlightsImmediately() + fast drawer teardown first
DeckFlowPhasebuilder-dismissingtableMorph-back onLayoutAnimationComplete; focus restored to originating blueprint
DeckFlowPhase(hard load)builder-openDirect load of /deck/[slug] — no origin, open morph skipped; close still morphs back
FlightRecordqueuedmeasuringDequeue (add-all stagger 70 ms) or immediate for single tap
FlightRecordmeasuringin-flightSource rect measured (scrollIntoView + re-measure next rAF if slot off-screen); clone mounts in FlightLayer
FlightRecordin-flightsettlingImperative spring resolves (.then()); clone removed; slot ghost→filled
FlightRecordsettlingdonePulse complete; displayedCount ticks
FlightRecordany non-donedonefinishAllFlightsImmediately() — snap-to-end, counter reconciled

7. Storyboards & Mechanism Decision

Section feedback:

All timings pass through dbg.scaleT and are debug-speed scalable.

blueprint-morph open

  1. Tap → pushState + blueprint claims opacity 0 / panel claims layoutId.
  2. Layout spring morph (~450 ms, framer layout spring).
  3. onLayoutAnimationComplete (one-shot ref) → builder-open.
  4. Content stagger-in (slot columns then shelf, ~200 ms, 40 ms stagger).

blueprint-morph close

  1. Content near-instant fade-out (0.01 s, so the morph reads).
  2. AnimatePresence onExitComplete → blueprint reclaims id.
  3. Morph-back (~350 ms) → onLayoutAnimationCompletetable, focus restored.

Morph-back completion frame is the flash-risk apex. It gets a dedicated gate step in the debug harness and a named manual slow-motion check in the proof gate.

card-flight

  1. Tap → optimistic commit + fan card dims (same frame).
  2. Measure source rect; if the target slot is off-screen, scrollIntoView({behavior:"instant"}) then re-measure next rAF.
  3. Clone mounts in FlightLayer at source rect.
  4. Imperative animate() x/y/scale to slot rect, spring {stiffness: 260, damping: 26} (~400 ms).
  5. .then(): clone removed, slot ghost→filled, declarative pulse scale [1, 1.08, 1] ~250 ms, counter ticks.

Add-all: filtered queue, 70 ms stagger per flight (13-card worst case ≈ 0.84 s launch tail), batch-complete via decrement ref; single tap is a batch of 1.

Mechanism decision — portal clone overlay (FlightLayer): the real fan card must stay in the fan as the dimmed badge, and a sheet child can never paint above the slot strip outside the sheet's z-50 stacking context; clone + fixed layer needs no unclipping and no scroll-offset math. The layoutId+declarative-animate conflict (PackOpener.tsx:212-213) is why the clone is driven imperatively with motion values, and why slot cards (which need declarative pulses) never carry a layoutId.

8. Implementation Guardrails (motion-framer subskill + baseline)

Section feedback:

Gate: scope / non-goals

The two new transitions ship with full reduced-motion behavior, but retrofitting prefers-reduced-motion support onto the existing pack flow (SealedPack tear, fan-out, collapse) is a named follow-up and out of scope for this plan. Confirm?

9. Debug-Harness Extension

Section feedback:

New step arrays in steps.ts (+ nodes/lanes deck-shell, builder, flight-layer and runtime slices in animationMachine.ts):

Step arraySteps
DECK_OPEN_STEPSblueprint-tapurl-pushbuilder-mountblueprint-morph-in (gate) → builder-content-in
DECK_CLOSE_STEPSdismiss-triggerflights-flushed (cond.) → drawer-fast-teardown (cond.) → builder-exit (gate) → blueprint-morph-out (gate, apex) → table-restored
FLIGHT_STEPSflight-tapflight-measureflight-launch (gate) → flight-land (gate) → flight-batch-complete

New debug drivers: openDeck, dismissDeck, flyCard, flyAll, extended reset. All new transitions pass through dbg.scaleT.

10. Proof Gate

Section feedback:

User direction during planning: add Playwright at implementation time.

Vitest sequence tests (existing pattern — mocked debug report() channel, mocked getBoundingClientRect)

Playwright (devDependency added in the deck-builder implementation phase, local runs only — no GitHub Actions)

Manual slow-motion review (at --debug-speed 0.25)

Acceptance: every "never" item in contracts A/B has a corresponding automated or named-manual check; onExitComplete-dependent cleanup verified to run after exit, not during.

Gate: proof tooling

Confirm adding Playwright as a devDependency during the deck-builder implementation phase (local runs only, no GitHub Actions), alongside the Vitest sequence tests and the named manual slow-motion checks?

11. Assumptions & Risk Register

Section feedback:
Risk / assumptionStatusMitigationWhat would change the plan
Next 16 shallow-pushState spike — shallow window.history.pushState + usePathname/popstate behavior on Next 16.2.6Load-bearing, unproven~30 min spike before any implementationIf shallow pushState fights the App Router, the routing layer is redesigned (e.g. full client-canvas with route only on hard load) before any animation work starts
Morph legibility across extreme aspect-ratio change (small blueprint → full panel)ProvisionalChrome-rectangle-only morph; content staggers in after landingIf the morph still reads poorly at slow motion, fall back to scale+fade with a shorter morph distance
Mobile slot-strip scroll-then-measure timing (scrollIntoView → re-measure next rAF)ProvisionalManual device check in the proof gateIf measurement races persist, pin the slot strip during flights
Scroll drift during a ~400 ms flight (user scrolls while clone is mid-air)Accepted limitationDocumented; clone targets the rect measured at launchOnly revisit if real-device review finds it disorienting

12. Artifact Destination & Proposed File Changes

Section feedback:

This run created (already committed with this page)

PathAction
prompts/animation-design-planner/skill-prompt-20260610-135323-deck-builder-transitions.mdcreate — prompt history log
research/skills-showcase/_working/preliminary-animation-design-planner-research.mdcreate — Stage 1 working packet (non-canonical)
alignment/animation-design-planner-deck-builder-transitions.htmlcreate — this review page
alignment/index.htmlupdate — add entry under Product Design & Spec

Post-approval mutations (a later turn, only after final compiled YAML)

PathAction
apps/skills-showcase/docs/animation-plan-deck-builder.mdcreate — canonical animation plan (this page's content, post-feedback)
docs/history/archive/YYYY-MM-DD/HHMMSS/research/skills-showcase/_working/…archive — working packet moved out of the active tree
alignment/animation-design-planner-deck-builder-transitions.htmlconvert — reviewconfirmed (prior page archived first)

No implementation files, no dependencies, and no changes to the unrelated in-flight skillpacks work in the tree.

Gate: artifact destination

Approve apps/skills-showcase/docs/animation-plan-deck-builder.md as the canonical destination for the approved animation plan (written only after approval, next to deck-builder-ux.md)?

Gate: proposed file changes

Approve the file-change scope and timing: this run's four review artifacts now, and the post-approval mutation set (archive the working packet, write the canonical plan, confirm this page) only after final compiled YAML?

13. Post-Approval Route

Section feedback:

After approval, the natural next step is the deck-builder implementation handoff: the canonical plan plus deck-builder-ux.md become the spec inputs, with the Next 16.2.6 pushState spike as the first implementation task.

Gate: post-approval route

After the canonical plan is written, how should the work route?

14. Compile YAML

Use Compile Feedback YAML to send emphasis requests, concerns, or clarification needs before answering every gate — feedback alone is a revision request, not approval. Use Compile Answers for final approval once all 9 required questions are answered.