1. phases
Itinera API
  • ✈ Itinera API Documentation
  • Docs
    • legacy
      • Itinera — System Overview
      • Technology Stack & Architecture
      • Getting Started Guide
      • Infrastructure
      • Frontend Application
      • Architecture Overview
      • API Reference
      • Backend Services
      • Development Guidelines
    • phases
      • 01. Architecture Overview & Checkout Idempotency
      • Phase 2: Commerce & Checkout Engine
      • Phase 3: Webhooks & Asynchronous Fulfillment
      • Phase 4: Security Perimeter & Authentication
      • Phase 5: Database Schema & Entity Relationships
      • Phase 6: AI Quota & Telemetry Subsystems
      • Phase 7: API Route Matrix & FormRequests
      • Phase 8: Global Exception & Error Handling
      • Phase 9: Frontend Ecosystem & State Management
      • Phase 10: Design System & Component Library
      • Phase 11: Interactive GSAP Animations
      • Phase 12: Deployment & CI/CD Pipeline
      • Phase 13: Testing Strategies
      • Phase 14: Performance & Optimization
      • Phase 15: Developer Onboarding & Runbooks
      • 15-Phase Comprehensive Wiki & Documentation Plan
  • APIs
    • Auth
      • Register a new user
      • Log in a user
      • Log out user
      • Refresh JWT token
      • Forgot password request
      • Reset password verification
      • Get current user profile
      • Update user profile
      • Redirect to Google OAuth
      • Google OAuth Callback
      • Verify email via signed URL
    • Catalog
      • List all countries
      • Get country details
      • List all cities
      • List all regions
      • List all destinations
      • Get destination details
      • Get hotels by destination
      • List all hotels
      • Get hotel details
      • Get reviews for a hotel
      • List all flights
      • Get flight details
      • List all restaurants
      • Get restaurant details
      • List all attractions
      • Get current weather
      • Submit review for an entity
      • Delete review
      • Toggle favourite status for entity
      • List my submitted reviews
    • Bookings
      • Book a tour destination
    • V1 Aliases
      • V1 List all countries
      • V1 Get country details
      • V1 List all cities
      • V1 List all destinations
      • V1 Get destination details
      • V1 Get hotels by destination
      • V1 List all hotels
      • V1 Get hotel details
      • V1 Get reviews for a hotel
      • V1 List all flights
      • V1 Get flight details
      • V1 List all restaurants
      • V1 Get restaurant details
      • V1 List all attractions
      • V1 Get attraction details
      • V1 List all regions
      • V1 Get weather details
    • Trips
      • List user trips
      • Create a new trip
      • Get trip details
      • Update trip details
      • Delete a trip
      • Get creation metadata
      • Attach items to a trip
      • Update trip item
      • Detach items from a trip
      • Fork a trip
    • Conversations
      • List user conversations
      • Start a new conversation
      • Get conversation details
      • List messages in conversation
      • Send message to conversation
      • Mark conversation as read
    • Commerce Plans
      • List public plans
      • Get public plan details
    • Commerce Subscriptions
      • Subscribe to a plan
      • Upgrade active plan
      • Get active subscription info
      • Cancel active subscription
    • Commerce Checkout
      • Initiate Paymob payment checkout
    • Integrations
      • Paymob status webhook callback
      • Paymob redirect return callback
    • System Settings & Support
      • Submit public contact message
      • Subscribe to system newsletter
      • Get list of my reports
      • List all notifications
      • Mark single notification as read
      • List available surveys
      • Submit answers for survey
      • Get survey details
      • Update survey details
      • Delete survey response
    • AI Tools
      • Enhance itinerary details using AI
      • Request AI review of itinerary
      • Plan route using AI assistance
      • Get AI quota remaining details
      • Chat with AI Concierge assistant
      • Get AI Review progress by ID
    • Agency Integration
      • Request agency assignment
      • List agency active tasks
      • List agency managed trips
      • Get agency total earnings
      • Get agency profile details
      • Update agency profile details
    • Admin User Management
      • List users inside admin dashboard
      • Get user profile
      • Set user active status
      • Block user profile
    • Admin Catalog Moderation
      • Create new catalog category
      • Create new catalog destination
      • Create new hotel catalog record
      • Create new flight catalog record
      • Create new restaurant catalog record
      • Create new attraction catalog record
  • Schemas
    • User
    • ErrorResponse
    • Trip
    • Destination
    • Hotel
    • Flight
    • Restaurant
    • Attraction
    • Booking
    • Review
    • Agency
    • Survey
  1. phases

Phase 11: Interactive GSAP Animations

This document deconstructs every gsap / ScrollTrigger timeline that powers the homepage reveal batch and the lifecycle trace page's scroll-driven engine. All motion is registered once in src/lib/gsap.ts and gated for headless, reduced-motion, and escape paths.

1. Registration — src/lib/gsap.ts:1-7#

import gsap from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
import { MotionPathPlugin } from "gsap/MotionPathPlugin"
gsap.registerPlugin(ScrollTrigger, MotionPathPlugin)
export { gsap, ScrollTrigger, MotionPathPlugin }
Single import guarantees one gsap instance — vitest mocks and ChapterScene.test.tsx assert against the same singleton.

2. Lifecycle Trace Page — src/pages/LifecyclePage.tsx#

2.1 Principle: Scroll Owns the Playhead#

No input hijack, no isTransitioning lock. Each of the 10 stage-* sections creates a scrubbed pin whose reveal timeline is bound to scrollTrigger inside the timeline config. Native #scroller scroll distance drives progress.
// LifecyclePage.tsx:347 — gsap.context() so StrictMode revert restores inline styles
const animCtx = gsap.context(() => { ... }, root)
immediateRender stays true (default): sections sit pre-hidden and are revealed by the scrub — no finished-state flash before the pin engages.

2.2 Stage Timeline — buildStageTimeline:348-382#

const tl = gsap.timeline({ defaults:{ease:"power2.out"}, scrollTrigger: stVars })
tl.from(icon-wrap/stage-tag/section-title/desc p/chip/artifact, {opacity:0,y:18,stagger:.06,duration:.6}, 0)
  .from(panel, {opacity:0,y:30,duration:.6}, .15)
  .to(draws, {strokeDashoffset:0,duration:.9,stagger:.035,ease:"power2.inOut"}, .4)
  .from(lc-fade, {autoAlpha:0,duration:.35,stagger:.03}, .75)
  // motionPath pulse dot — circle appended to each .lc-flow path
  .fromTo(dot,{opacity:0},{opacity:1,duration:.15}, at)
  .to(dot,{motionPath:{path,align:path,alignOrigin:[.5,.5]},duration:1.15,ease:"power1.inOut"}, at)
  .to(dot,{opacity:0,duration:.15})
prepDraws:327-338 primes every .lc-draw (strokeDasharray = len, strokeDashoffset = len) so the to can draw.
Dots are created with createElementNS("http://www.w3.org/2000/svg","circle") and appended to the path parent — cleaned on unmount querySelectorAll(".lc-pulse-dot").forEach(d=>d.remove()).
Per-stage stVars (385-404):
{ trigger: sec, scroller: scrollerEl, start:"top top", end:"+=80%",
  pin:true, pinSpacing:true, anticipatePin:1, scrub:0.45,
  onToggle: self => { if(!self.isActive) return; activate(sec); currentIdx = STAGE_IDS.indexOf(id)+1 } }
The pin trigger owns chrome activation — a separate top center trigger on a pinned element mismeasures and makes the rail regress mid-scroll.

2.3 Hero — Entrance Once + Exit Scrub#

Entrance — gsap.timeline({delay:.1}).from(title line span {yPercent:110, stagger:.09}) + from(hero-art {opacity:0,x:60}) + from(scroll-cue).
Exit — gsap.to(heroTitle/hero-art/scroll-cue {opacity:0,y:-40, scrub:true, trigger:"#lc-hero", start:"top top", end:"bottom top", onToggle: activate(hero)}).

2.4 Outro — Full Tail Scrub#

gsap.timeline({ scrollTrigger:{trigger:"#outro", scroller, start:"top 85%", end:"bottom bottom", scrub:true, onToggle: activate(outro)}})
  .fromTo("#outro status-final/hero-title/hero-sub/chip {opacity:0,y:26} → {opacity:1,y:0,stagger:.08}", ...)
end:"bottom bottom" owns the tail through maxScroll — the old top 30% left the final stretch deactivated so wrapping up from hero landed on a dead rail.

2.5 Reduced-Motion Branch — if(reducedMotion) vs if(!reducedMotion)#

LifecyclePage.tsx:216 ignores OS prefers-reduced-motion intentionally (showcase always animates). Only QA opt-out ?motion=reduced flips the path:
!reducedMotion builds the 10 scrub pins + hero entrance/exit + outro scrub (above).
reducedMotion builds 12 plain triggers start:"top center", end:"bottom center", onToggle: activate — rail still live, zero pins, instant goTo (behavior: auto).

2.6 Nav & Chrome Sync#

startOffset:320 — Map<string,number> of trigger.id → st.start. refreshStarts:488 sorts stInstances with Number(b.animation!=null) - Number(a.animation!=null) so pin/scrub starts beat top center activation triggers (300px priority).
goTo:501 — mapped ?? fallback scrollerEl.scrollTop + rect delta, scrollTo({behavior: reducedMotion?"auto":"smooth"}).
activate:270 sets --accent, typewrites dataset.trace (14ms char), updates counter ring strokeDashoffset = 100-pct, rail --rail-pct, progress bar width, and history.replaceState(?stage=id) — sandboxed try/catch.

2.7 Infinite Wrap — Intent-Gated, Animated#

Never from resting positions. Thresholds WRAP_DELTA_WHEEL=40, WRAP_DELTA_TOUCH=8:
handleEdgeWheel: e.deltaY ≥40 && atBottom → wrapAnimate(2) (top), e.deltaY ≤-40 && atTop → wrapAnimate(maxScroll-4)
wrapAnimate: gsap.to(scroller, {scrollTop:top, duration:reducedMotion?0:1.05, ease:"power2.inOut"})
Touch: handleDirStart/Move tracks touchDirY, handleEdgeTouch checks abs(touchDirY) <8 → ignore, goingDown = touchDirY<0. Guard lastEdgeWrapAt 400ms debounce.

2.8 Cleanup — return () => { ... }:621-641#

clearTimeout(deepLinkTimer/resizeTimer), remove load/keydown/wheel/touch listeners, clearInterval(typeTimer), animCtx.revert() (kills STs+timelines AND restores inline styles — critical so StrictMode pass #2 doesn't record hidden styles), stInstances.length=0, remove .lc-pulse-dot, removeProperty("--accent"), ScrollTrigger.defaults({scroller:null}), restore history.scrollRestoration.

2.9 Headless & Fallback Guarantees#

Unfocused window → requestAnimationFrame sleeps: only gsap.set progresses. Lifecycle never relies on to for visibility (scrub needs scroll); the only to is the scrubbed timeline and the animated wrapAnimate (tested via forced stInstances push).
Fonts: document.fonts.ready.then(()=>refresh+refreshStarts) + load + setTimeout 220ms recalc.

3. Homepage — src/pages/Home.tsx:178-219#

Batch reveal uses a single gsap.context:
gsap.set(".fe-flip,.hard-card,.demo-card,.dep-step",{autoAlpha:0,y:16})
ScrollTrigger.batch(".fe-flip,...",{start:"top 88%", once:true,
  onEnter:(batch)=>gsap.to(batch,{autoAlpha:1,y:0,duration:.5,stagger:.06,clearProps:"transform"})})
Headers of completed conferences were still visible as background art to prove the year had passed before the user scrolled, creating the desired depth rather than relying on a generic parallax library.
Safety nets (browser ticker may sleep, anchor hash may jump past top 88%):
2.5s gsap.to(..., {autoAlpha:1,y:0,duration:.4,clearProps:"transform"}) — animated catch-up.
4s gsap.set(..., {autoAlpha:1,y:0}) — ticker-free forced completion.
useIsReducedMotion exists but the showcase ignores it; section three honors it only via .motion-reduced class (Home.tsx:437 ?motion=reduced), and flip cards honor matchMedia("(prefers-reduced-motion: reduce)") in CSS (Transition: none; animation: none under .motion-reduced).

4. Chapter Scenes — src/components/lifecycle/ChapterScene.tsx:9-308#

Isolated SEC = var(--lc-sec), TXT = var(--lc-ink), DIM = var(--lc-mut) — light remap via html.light .lifecycle-root. Per-chapter SVG viewBox 0 0 900 420:
.lc-draw — geometric strokes, prepped by prepDraws, drawn via to({strokeDashoffset:0}).
.lc-fade — labels/chips, from({autoAlpha:0}).
.lc-flow / data-flow="1" — pulse dot path; dot appended per flow, driven by motionPath.

5. CSS Motifs & Micro-Motion#

FeMotifs (§03) — src/index.css:492-547: fx-boot 4.6s, fx-wave 1.05s, fx-tilt3d 3.2s, fx-radar-sweep 2.6s conic-gradient, fx-toast-up 5s, fx-pass-print 5.2s plane 6%→100%-22px. Gate: .fe-flip .fe-motif {paused} → :hover/:focus/.is-flipped {running}.
SecurityMotifs (§04) — 766-714: fill-up, shield-flash, countdown 301 dashoffset, move-dot, pulse-glow, fade-in-row.
Substrate & Marquee: trace-flow 1.4s dashoffset -18, hero-marquee 24s translateX -16.6% paused on hover.

6. Motion Diagram#

7. Principles#

1.
Scroll owns scrub, not JS. No wheel hijack, no isTransitioning lock — native scroller scroll drives scrub:0.45 progress; wrap is the only gsap.to(scroller,scrollTop).
2.
Context is the cleanup. gsap.context(...,root) + revert() kills triggers, timelines, inline styles and restores the DOM so StrictMode pass #2 doesn't record hidden as natural.
3.
Pin trigger owns activation. Storing activation on the pin trigger (not a second top center trigger) prevents rail regression mid-pin.
4.
Ticker-independent completion. gsap.set after 4s guarantees visibility when requestAnimationFrame sleeps in headless/background tabs.
5.
Showcase ignores reduced-motion, QA opts in. OS setting is intentionally ignored; ?motion=reduced is the only kill-switch, wired before registration.
Modified at 2026-08-25 22:41:45
Previous
Phase 10: Design System &amp; Component Library
Next
Phase 12: Deployment &amp; CI/CD Pipeline
Built with