"use client"; import React, { useRef, useEffect, useState } from 'react' import Experience from './components/Experience' import ScrollRig from './components/ScrollRig' import Navbar from './components/ui/Navbar' import FirstMile from './components/sections/FirstMile' import MidMile from './components/sections/MidMile' import LastMile from './components/sections/LastMile' import Analytics from './components/sections/Analytics' import { useSceneStore } from './store/useSceneStore' import './styles/experience.css' import Lenis from 'lenis' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' gsap.registerPlugin(ScrollTrigger) /** * Experience3D * --------------------------------------------------------------------------- * The full scroll-driven 3D logistics story, ported from the standalone Vite * app's App.jsx and embedded as the body of the How It Works page (below the * existing Elementor hero, above the global Footer). * * Two integration changes vs. the standalone app: * 1. Self-managed fixed pin. The site has a fixed header and an ancestor with * `overflow:hidden`, both of which break CSS `position: sticky`. So this is * a tall `position:relative` section (`.dm-hiw-3d`, its height supplied by * the 900vh ScrollRig spacer) with an absolutely-positioned `.dm-hiw-3d-stage` * toggled absolute(top) → fixed → absolute(bottom) via the ScrollTrigger pin * state — the same approach the site's other 3D sections use (StrategySection). * 2. The global Lenis is disabled on `/how-it-works` (SmoothScroll.tsx) so the * experience runs its own tuned Lenis here without a second instance fighting * it. The internal "Scroll to start" Hero overlay is dropped because the page * keeps the Elementor HowItWorksHero above this section. */ export default function Experience3D() { const scrollProgress = useSceneStore((state) => state.scrollProgress) const setLenis = useSceneStore((state) => state.setLenis) const containerRef = useRef(null) const [pinState, setPinState] = useState('before') // Defer mounting the WebGL Canvas until the section nears the viewport. This // mirrors the site's other 3D sections (StrategySection's `mountScene`): besides // saving the heavy 32MB scene until needed, it keeps the Canvas out of React // StrictMode's initial synchronous double-mount, which otherwise creates and // immediately loses the WebGL context in dev ("THREE.WebGLRenderer: Context Lost"), // leaving a blank canvas. Once mounted it stays mounted. const [mountScene, setMountScene] = useState(false) useEffect(() => { const el = containerRef.current if (!el) return const io = new IntersectionObserver( (entries) => { if (entries.some((e) => e.isIntersecting)) { setMountScene(true) io.disconnect() } }, { rootMargin: '200% 0px' }, // mount well before it scrolls into view ) io.observe(el) return () => io.disconnect() }, []) // Own Lenis instance (global Lenis is gated off for this route). useEffect(() => { const lenis = new Lenis({ duration: 1.2, lerp: 0.08, syncTouch: true, }) setLenis(lenis) lenis.on('scroll', ScrollTrigger.update) let rafId function raf(time) { lenis.raf(time) rafId = requestAnimationFrame(raf) } rafId = requestAnimationFrame(raf) gsap.ticker.lagSmoothing(0) ScrollTrigger.refresh() return () => { cancelAnimationFrame(rafId) lenis.destroy() setLenis(null) } }, [setLenis]) // 3D references shared between R3F and the GSAP scroll system. const truckRef = useRef(null) const wheelRefs = React.useMemo(() => [ { current: null }, // FR { current: null }, // FL { current: null }, // RL { current: null }, // RR ], []) const dashboardRefs = React.useMemo(() => ({ bars: [ { current: null }, { current: null }, { current: null }, { current: null }, { current: null }, { current: null } ], floorBars: [ { current: null }, { current: null }, { current: null }, { current: null }, { current: null } ], pieQuarters: [ { current: null }, { current: null }, { current: null }, { current: null } ] }), []) return (
{/* Pinned stage: canvas + HTML overlays. Stays fixed across the scroll. */}
= 0.92 ? 0.85 : 1.0, transition: 'opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1)', }} > {mountScene && ( )}
{/* In-experience section navigation */} {/* Story stage text panels (revealed at their scroll ranges) */}
= 0.02 && scrollProgress < 0.14} /> = 0.38 && scrollProgress < 0.50} /> = 0.80 && scrollProgress < 0.92} /> = 0.94} />
{/* GSAP scroll system: 900vh in-flow spacer that gives the section its height, drives scroll progress, and reports pin state. */}
) }