);
}
/** Hover-glow + subtle mouse-parallax tilt wrapper (Framer Motion). */
function TiltCard({ className = "", children }: { className?: string; children: React.ReactNode }) {
const reduce = useReducedMotion();
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useSpring(useTransform(y, [-40, 40], [7, -7]), { stiffness: 240, damping: 22 });
const rotateY = useSpring(useTransform(x, [-40, 40], [-7, 7]), { stiffness: 240, damping: 22 });
return (
{
if (reduce) return;
const r = e.currentTarget.getBoundingClientRect();
x.set(e.clientX - r.left - r.width / 2);
y.set(e.clientY - r.top - r.height / 2);
}}
onMouseLeave={() => {
x.set(0);
y.set(0);
}}
whileHover={{ y: -4 }}
transition={{ type: "spring", stiffness: 260, damping: 22 }}
>
{children}
);
}
/** Framer-Motion count-up (distinct from the GSAP CountUp used in Section 10). */
function FCount({ to, decimals = 0, suffix = "" }: { to: number; decimals?: number; suffix?: string }) {
const ref = useRef(null);
const inView = useInView(ref, { once: true, margin: "-10% 0px" });
const mv = useMotionValue(0);
const [display, setDisplay] = useState((0).toFixed(decimals) + suffix);
useEffect(() => {
const unsub = mv.on("change", (v) => setDisplay(v.toFixed(decimals) + suffix));
return unsub;
}, [mv, decimals, suffix]);
useEffect(() => {
if (!inView) return;
const controls = animate(mv, to, { duration: 1.5, ease: [0.16, 1, 0.3, 1] });
return controls.stop;
}, [inView, to, mv]);
return {display};
}
/** Wheel + ground-shadow primitives for VehicleArt — module-level (not
* declared inside a render body) so the React Compiler doesn't treat them
* as freshly-created components on every render. */
function VWheel({ cx, rim }: { cx: number; rim: string }) {
return (
<>
>
);
}
function VShadow() {
return ;
}
function VDefs() {
return (
);
}
/** Vehicle illustration built from primitives (filled body/glass/wheels + ground
* shadow) so it reads as a small rendered vehicle rather than a line icon. */
function VehicleArt({ type, active }: { type: "bike" | "ev" | "mini" | "van" | "truck"; active: boolean }) {
const body = active ? "url(#swVBodyActive)" : "#4b4b54";
const glass = active ? "rgba(255,255,255,0.5)" : "rgba(255,255,255,0.28)";
const wheelRim = active ? "rgba(226,53,74,0.55)" : "rgba(255,255,255,0.2)";
if (type === "bike") {
return (
);
}
if (type === "ev") {
return (
);
}
if (type === "mini") {
return (
);
}
if (type === "van") {
return (
);
}
return (
);
}
/* ============================================================
SECTION 01 — The AI Logistics Engine
Not a diagram, not a timeline: one dominant, always-alive engine
core. An order travels in, the engine visibly analyzes it, then
dispatches three coordinated outcomes at once, closing on delivery.
Answers "what happens after I place an order?" in one glance.
============================================================ */
function Sec01() {
const outputs = [
{ icon: IconDriver, label: "Fleet Driver", note: "Nearest available operator" },
{ icon: IconTruck, label: "Vehicle", note: "Bike, EV, van or truck" },
{ icon: IconRoute, label: "Route", note: "Optimized for time & cost" },
];
const readoutRows = ["Reads the order", "Picks the fleet driver & vehicle", "Plans the best route"];
const wrapRef = useRef(null);
const orderRef = useRef(null);
const c1Ref = useRef(null);
const engineRowRef = useRef(null);
const coreTagRef = useRef(null);
const readoutDotRefs = useRef<(HTMLSpanElement | null)[]>([]);
const readoutCheckRefs = useRef<(HTMLSpanElement | null)[]>([]);
const readoutRowRefs = useRef<(HTMLDivElement | null)[]>([]);
const fanRefs = useRef<(SVGPathElement | null)[]>([]);
const outNodeRefs = useRef<(HTMLDivElement | null)[]>([]);
const c2Ref = useRef(null);
const trackingRef = useRef(null);
const c3Ref = useRef(null);
const deliveredRef = useRef(null);
useEffect(() => {
const wrap = wrapRef.current;
const order = orderRef.current;
const c1 = c1Ref.current;
const engineRow = engineRowRef.current;
const coreTag = coreTagRef.current;
const readoutDots = readoutDotRefs.current.filter(Boolean) as HTMLSpanElement[];
const readoutChecks = readoutCheckRefs.current.filter(Boolean) as HTMLSpanElement[];
const readoutRowsEl = readoutRowRefs.current.filter(Boolean) as HTMLDivElement[];
const fanLines = fanRefs.current.filter(Boolean) as SVGPathElement[];
const outNodes = outNodeRefs.current.filter(Boolean) as HTMLDivElement[];
const c2 = c2Ref.current;
const tracking = trackingRef.current;
const c3 = c3Ref.current;
const delivered = deliveredRef.current;
if (!wrap || !order || !c1 || !engineRow || !coreTag || fanLines.length === 0 || !c2 || !tracking || !c3 || !delivered) return;
const coreStatus = ["READING…", "CHOOSING…", "PLANNING…"];
const fanLens = fanLines.map((p) => p.getTotalLength());
gsap.set(fanLines, { strokeDasharray: (i: number) => fanLens[i], strokeDashoffset: (i: number) => fanLens[i] });
gsap.set(order, { opacity: 0, y: -14, scale: 0.9 });
gsap.set([c1, c2, c3], { "--fill": 0 } as gsap.TweenVars);
gsap.set(readoutDots, { opacity: 1 });
gsap.set(readoutChecks, { opacity: 0, scale: 0.4 });
gsap.set(readoutRowsEl, { opacity: 0.4 });
gsap.set(outNodes, { opacity: 0.28, y: 10, scale: 0.94 });
gsap.set(tracking, { opacity: 0.28, y: 10, scale: 0.94 });
gsap.set(delivered, { opacity: 0, scale: 0.7 });
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
if (reduced) {
gsap.set(order, { opacity: 1, y: 0, scale: 1 });
gsap.set([c1, c2, c3], { "--fill": 1 } as gsap.TweenVars);
gsap.set(fanLines, { strokeDashoffset: 0 });
gsap.set(readoutDots, { opacity: 0 });
gsap.set(readoutChecks, { opacity: 1, scale: 1 });
gsap.set(readoutRowsEl, { opacity: 1 });
gsap.set(outNodes, { opacity: 1, y: 0, scale: 1 });
gsap.set(tracking, { opacity: 1, y: 0, scale: 1 });
gsap.set(delivered, { opacity: 1, scale: 1 });
return;
}
const play = () => {
const tl = gsap.timeline();
// Order arrives, then travels into the engine.
tl.to(order, { opacity: 1, y: 0, scale: 1, duration: 0.45, ease: "back.out(2)" }, 0)
.to(c1, { "--fill": 1, duration: 0.5, ease: "power2.in" } as gsap.TweenVars, 0.5)
// The order stays on screen as the top of the flow (not a one-shot
// pulse that disappears) so the diagram reads top-to-bottom even
// to someone who arrives after the intro has already played.
// The engine visibly thinks: orbit + waveform speed up, and the
// readout ticks off analysing -> comparing -> selecting one at a time.
.add(() => engineRow.classList.add("is-active"), 1.05);
readoutRowsEl.forEach((row, i) => {
const at = 1.2 + i * 0.4;
// The core's own status readout — not just the side panel — cycles
// through what it's doing, so the "thinking" is legible even to
// someone glancing only at the core itself.
tl.call(() => { coreTag.textContent = coreStatus[i] ?? coreStatus[coreStatus.length - 1]; }, [], at)
.to(row, { opacity: 1, duration: 0.2 }, at)
.to(readoutDots[i], { opacity: 0, scale: 0.4, duration: 0.18 }, at + 0.16)
.to(readoutChecks[i], { opacity: 1, scale: 1, duration: 0.3, ease: "back.out(3)" }, at + 0.16);
});
const afterReadout = 1.2 + readoutRowsEl.length * 0.4 + 0.3;
tl.call(() => { coreTag.textContent = "DECIDED"; }, [], afterReadout - 0.15)
.call(() => { coreTag.textContent = "Doormile AI"; }, [], afterReadout + 0.5)
.add(() => engineRow.classList.remove("is-active"), afterReadout)
// Three coordinated outcomes dispatch together.
.to(fanLines, { strokeDashoffset: 0, duration: 0.5, stagger: 0.14, ease: "power2.out" }, afterReadout + 0.1)
.to(outNodes, { opacity: 1, y: 0, scale: 1, duration: 0.4, stagger: 0.14, ease: "back.out(2.2)" }, afterReadout + 0.1)
// Then live tracking, then delivered.
.to(c2, { "--fill": 1, duration: 0.4, ease: "power2.inOut" } as gsap.TweenVars, afterReadout + 0.75)
.to(tracking, { opacity: 1, y: 0, scale: 1, duration: 0.4, ease: "back.out(2.2)" }, afterReadout + 0.95)
.to(c3, { "--fill": 1, duration: 0.35, ease: "power2.inOut" } as gsap.TweenVars, afterReadout + 1.4)
.to(delivered, { opacity: 1, scale: 1, duration: 0.4, ease: "back.out(2.4)" }, afterReadout + 1.65);
};
const st = ScrollTrigger.create({ trigger: wrap, start: "top 75%", once: true, onEnter: play });
return () => st.kill();
}, [outputs.length, readoutRows.length]);
return (
What happens after a customer places an order>}
lede="One AI engine reads every order, decides the fleet driver, the vehicle and the route, then stays with the shipment until it's signed for."
/>
{IconBag}Customer Places OrderAny channel, one order
{/* A single, calm AI node — the Doormile mark plus a short status
label — instead of the earlier busy processor (orbiting dots,
corner brackets, radar sweep, waveform). Easier to read at a
glance: this is simply "the AI deciding". */}
{/* eslint-disable-next-line @next/next/no-img-element */}
Doormile AI
Doormile AI Logistics Engine
{readoutRows.map((r, i) => (
);
}
/** Premium filled (not outline) illustrations for the five pickup
* checkpoints — two-tone red/steel shapes rather than thin-stroke icons. */
function PickupIcon({ kind }: { kind: "request" | "driver" | "arrival" | "otp" | "collected" }) {
if (kind === "request") {
return (
);
}
if (kind === "driver") {
return (
);
}
if (kind === "arrival") {
return (
);
}
if (kind === "otp") {
return (
);
}
return (
);
}
/** One wheel: a hub + realistic 5-spoke rim so rotation is actually visible
* (a plain circle would spin invisibly). `sw-van-wheel` is rotated directly
* every frame in Sec02 — transformBox/transformOrigin make the CSS
* transform rotate around the hub, not the SVG's (0,0) origin. */
/** `front` marks the wheel Sec02 gives an extra small steering rotation to
* on top of its continuous spin — the van always travels leading with this
* wheel (see the flip/tilt math in Sec02, which orients the sprite so
* cx=100 always faces the direction of travel). */
function VanWheel({ cx, front }: { cx: number; front?: boolean }) {
return (
{/* Spoke offsets are precomputed (not Math.cos/sin at render time) —
trig can round to a different last digit between the server and
client JS engines, which React's hydration check flags as a
mismatch even though it's visually meaningless. */}
{[
{ dx: 6.6, dy: 0 },
{ dx: 2.04, dy: 6.28 },
{ dx: -5.34, dy: 3.88 },
{ dx: -5.34, dy: -3.88 },
{ dx: 2.04, dy: -6.28 },
].map(({ dx, dy }) => (
))}
);
}
/** Shared gradient/filter defs for VanArt — module scope so they aren't
* redeclared (and re-IDed) on every render. */
function VanDefs() {
return (
);
}
/** A premium, filled delivery-van illustration — body panels, cab glass with
* a reflection streak, side mirror, head/tail lights and realistic-rim
* wheels that actually spin (see VanWheel) — that physically rides the
* pickup route (see Sec02 below). Cargo box (rear) sits over cx=26, the cab
* and hood (front) over cx=100 — the sprite's native, unflipped pose noses
* toward cx=100, the side the flip/tilt math in Sec02 treats as the
* direction of travel. The ground shadow is rendered as a separate element
* outside this SVG (sw-pickup-van-shadow) so it can stay flat on the road
* while the body above it leans and bounces. Rim-light and contact-shadow
* are applied as a filter on .sw-van-svg (see CSS) rather than per-shape
* outlines, so every part of the silhouette gets the same edge without
* duplicating each path. */
function VanArt() {
return (
);
}
/* ============================================================
SECTION 02 — Smart Pickup Management
Not another progress timeline: an illustrated delivery van physically
drives a route drawn over the pickup photo, accelerating and decelerating
between five real checkpoints (AI-coordinated, not a generic list).
============================================================ */
/** Waypoints the van drives through, in section-relative percent, in the
* same order as `stops` below (Pickup Request -> ... -> Package Collected)
* so checkpoint reading order, road-progress direction and van travel
* direction all agree — left to right, like Google Maps navigation. The
* photo (see sw-pickup-img object-position below) is framed so the open
* road runs across its lower half with the van's open door at the left —
* these points trace a single road-perspective curve along that pavement
* (monotonic in both x and y: near/left is lower and closer, distant/right
* is higher and further out) rather than zig-zagging between arbitrary
* heights, so it reads as one route instead of a glitch line. Starts at the
* van's open door, where the destination pin sits and the delivery van
* departs from. Straight segments between them are interpolated directly as CSS
* left/top percentages (not CSS offset-path — bare path() coordinates are
* pixel units, not container-relative, so they don't scale with a
* responsive container). The whole group sits ~16 points higher than the
* raw pavement trace would put it (max y 68, not 84) so there's a real
* Photo -> Road -> Van -> whitespace -> section-end margin at the bottom
* instead of the lowest checkpoint's label crowding (or clipping against)
* the .sw-edge overflow:hidden boundary. */
const PICKUP_PTS = [
{ x: 18, y: 68 }, { x: 31, y: 64 }, { x: 49, y: 59 }, { x: 70, y: 53 }, { x: 91, y: 47 },
];
const PICKUP_DEST = { x: PICKUP_PTS[0].x, y: PICKUP_PTS[0].y - 15 };
/** Rounds each interior joint of a polyline into a short quadratic-curve
* corner (clamped to a fraction of the shorter adjacent segment so corners
* never overlap) — a believable turning radius instead of the sharp,
* decorative-looking angles a raw polyline draws at every waypoint. */
function roundedRouteD(points: { x: number; y: number }[], radius: number): string {
if (points.length < 3) return "M " + points.map((p) => `${p.x} ${p.y}`).join(" L ");
let d = `M ${points[0].x} ${points[0].y} `;
for (let i = 1; i < points.length - 1; i++) {
const prev = points[i - 1];
const curr = points[i];
const next = points[i + 1];
const v1x = curr.x - prev.x, v1y = curr.y - prev.y;
const v2x = next.x - curr.x, v2y = next.y - curr.y;
const len1 = Math.hypot(v1x, v1y);
const len2 = Math.hypot(v2x, v2y);
const r = Math.min(radius, len1 * 0.42, len2 * 0.42);
const p1x = curr.x - (v1x / len1) * r, p1y = curr.y - (v1y / len1) * r;
const p2x = curr.x + (v2x / len2) * r, p2y = curr.y + (v2y / len2) * r;
d += `L ${p1x} ${p1y} Q ${curr.x} ${curr.y} ${p2x} ${p2y} `;
}
const last = points[points.length - 1];
d += `L ${last.x} ${last.y}`;
return d;
}
/** The actual drawn/ridden road — rounded corners at every checkpoint so
* turns read as a real navigation route (Google Maps/Uber style) instead
* of an angular decorative polyline. Checkpoint markers still sit at the
* original sharp waypoints (PICKUP_PTS), which the rounded curve passes
* within `radius` units of. */
const PICKUP_ROAD_D = roundedRouteD(PICKUP_PTS, 4.5);
function Sec02() {
const stops = [
{ icon: , label: "Pickup Request", note: "Order raised from any source" },
{ icon: , label: "Nearest Driver", note: "AI matches the closest rider" },
{ icon: , label: "Driver Arrival", note: "Real-time ETA to origin" },
{ icon: , label: "OTP Verification", note: "Secure, tamper-proof handoff" },
{ icon: , label: "Package Collected", note: "Delivery lifecycle begins" },
];
const wrapRef = useRef(null);
const imgRef = useRef(null);
const vanRef = useRef(null);
const vanBounceRef = useRef(null);
const vanRotRef = useRef(null);
const vanShadowRef = useRef(null);
const routeProgressRef = useRef(null);
const stopRefs = useRef<(HTMLDivElement | null)[]>([]);
const confirmRef = useRef(null);
const confirmLabelRef = useRef(null);
const aiDotRef = useRef(null);
const aiCheckRef = useRef(null);
const aiTextRef = useRef(null);
const successGlowRef = useRef(null);
useEffect(() => {
const wrap = wrapRef.current;
const img = imgRef.current;
const van = vanRef.current;
const vanBounce = vanBounceRef.current;
const vanRot = vanRotRef.current;
const vanShadow = vanShadowRef.current;
const routeProgress = routeProgressRef.current;
const confirm = confirmRef.current;
const confirmLabel = confirmLabelRef.current;
const aiDot = aiDotRef.current;
const aiCheck = aiCheckRef.current;
const aiText = aiTextRef.current;
const successGlow = successGlowRef.current;
const stopEls = stopRefs.current.filter(Boolean) as HTMLDivElement[];
const wheelEls = van ? Array.from(van.querySelectorAll(".sw-van-wheel")) : [];
const frontWheelEl = van ? van.querySelector(".sw-van-wheel--front") : null;
if (!wrap || !img || !van || !vanBounce || !vanRot || !vanShadow || !routeProgress || !confirm || !confirmLabel || !aiDot || !aiCheck || !aiText || !successGlow || stopEls.length === 0) return;
const routeLen = routeProgress.getTotalLength();
gsap.set(routeProgress, { strokeDasharray: routeLen, strokeDashoffset: routeLen });
const activate = (i: number) => {
stopEls.forEach((el, k) => el.classList.toggle("is-active", k <= i));
};
// Sample the van's position AND heading directly off the rendered,
// rounded-corner road (same the progress line draws) instead of
// computing straight-segment math separately — guarantees the wheels
// stay glued to the visible curve through the turns, not just at
// waypoints. Symmetric before/after sampling gives a stable tangent.
const sampleRoad = (pct: number) => {
const clamped = Math.max(0, Math.min(100, pct));
const len = (clamped / 100) * routeLen;
const eps = Math.max(0.4, routeLen * 0.015);
const ptA = routeProgress.getPointAtLength(Math.max(0, len - eps));
const ptB = routeProgress.getPointAtLength(Math.min(routeLen, len + eps));
const pt = routeProgress.getPointAtLength(len);
const dx = ptB.x - ptA.x, dy = ptB.y - ptA.y;
const flip = dx < 0 ? -1 : 1;
const tilt = Math.atan2(dy, Math.abs(dx)) * (180 / Math.PI);
return { x: pt.x, y: pt.y, tilt, flip };
};
// Real-vehicle motion is derived from actual elapsed time between frames
// (not the tween's nominal duration, since scrub time is scroll-driven
// and can move at any real-world speed): wheel spin, steering and the
// ground shadow all react to genuine instantaneous speed, so they
// naturally ease in with the tween's acceleration and settle to a stop
// as it brakes into each checkpoint.
let lastPct = 0;
let lastTs = typeof performance !== "undefined" ? performance.now() : 0;
let lastTilt = 0;
let wheelDeg = 0;
let movingUntil = 0;
const place = (pct: number) => {
const now = typeof performance !== "undefined" ? performance.now() : lastTs + 16;
const dtMs = Math.max(1, now - lastTs);
const dPct = pct - lastPct;
const speed = Math.abs(dPct) / dtMs; // % of route per ms
lastPct = pct;
lastTs = now;
const p = sampleRoad(pct);
van.style.left = `${p.x}%`;
van.style.top = `${p.y}%`;
// scaleX flips the sprite to face its direction of travel (it's drawn
// facing right-ish by default); rotate applies only the small road-
// slope tilt, never the raw heading angle — a full atan2(dy,dx)
// rotate() would flip the van upside-down whenever a route bends back
// on itself. A CSS transition on this element (see
// .sw-pickup-van-rot) eases the tilt change with a slight overshoot,
// reading as banking into the turn instead of snapping.
vanRot.style.transform = `scaleX(${p.flip}) rotate(${p.flip * p.tilt}deg)`;
wheelDeg += dPct * 22;
const turnRate = p.tilt - lastTilt;
lastTilt = p.tilt;
const steer = Math.max(-16, Math.min(16, turnRate * 5));
for (const w of wheelEls) w.style.transform = `rotate(${wheelDeg}deg)`;
if (frontWheelEl) frontWheelEl.style.transform = `rotate(${wheelDeg + steer}deg)`;
const moving = speed > 0.0015;
if (moving) movingUntil = now + 160; // small grace period so bounce/speed-lines don't flicker off between frames
const isMoving = now < movingUntil;
vanBounce.classList.toggle("is-moving", isMoving);
van.classList.toggle("is-moving", isMoving);
// The shadow lives outside the rotate/bounce chain so it stays flat on
// the road while the body above it leans and bobs — a real contact
// shadow, not one that's rigidly glued to the sprite. Motion blur is
// deliberately NOT applied here — it was making the van harder to
// see while moving, which is the opposite of the goal; the speed
// lines (CSS, see .sw-pickup-van-speedlines) carry the sense of
// speed instead, without ever softening the van itself.
const shadowSpread = Math.min(0.3, speed * 6);
vanShadow.style.opacity = String(0.85 - shadowSpread);
vanShadow.style.transform = `scaleX(${1 + shadowSpread})`;
routeProgress.style.strokeDashoffset = `${routeLen * (1 - pct / 100)}`;
};
const showToast = (label: string) => {
confirmLabel.textContent = label;
gsap.fromTo(confirm, { opacity: 0, scale: 0.4 }, { opacity: 1, scale: 1, duration: 0.2, ease: "back.out(3)" });
};
const finish = () => {
aiText.textContent = "AI Coordinated Pickup";
gsap.to(aiDot, { opacity: 0, duration: 0.2 });
gsap.to(aiCheck, { opacity: 1, scale: 1, duration: 0.35, ease: "back.out(3)" });
vanBounce.classList.remove("is-moving");
van.classList.remove("is-moving");
// A brief, subtle success flash across the frame — the completed
// route and checkmarks stay put, this just punctuates the moment.
// The van's own fade-out lives as a tween on the main scrubbed
// timeline (not here) so scrolling back up reverses it smoothly —
// an imperative one-shot gsap.to() in this callback doesn't
// un-happen on scroll-back the way a scrubbed tween does.
gsap.fromTo(successGlow, { opacity: 0 }, { opacity: 1, duration: 0.25, ease: "power1.out", yoyo: true, repeat: 1 });
};
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
gsap.set(confirm, { opacity: 0, scale: 0.4 });
gsap.set(aiCheck, { opacity: 0, scale: 0.4 });
gsap.set(successGlow, { opacity: 0 });
place(0);
if (reduced) {
place(100);
activate(stops.length - 1);
finish();
return;
}
const proxy = { v: 0 };
// A fixed timeline "budget" (10 units, arbitrary but easy to reason
// about in tenths) split across stages so scroll % always means the
// same thing: 0-20% idle at Pickup Request, 20/40/60/80% continuous
// driving between the four remaining checkpoints, 95% arrival, and a
// 95-100% exit beat — never "scroll 40%, van still at 0%" or "van
// already parked at 80% with 20% of empty scroll left to go".
const IDLE = 2; // 0 -> 20%: waiting at Pickup Request (CSS idle-breathe/headlight-blink below, no JS needed)
const LEG = 2; // 20->40, 40->60, 60->80
const LEG4 = 1.5; // 80 -> 95
const EXIT = 0.5; // 95 -> 100: glow, badge, package-loaded toast, van fade
const CP_HOLD = 0.3; // brief full stop at a checkpoint before departing again — skipped on the very first leg, since the idle stage already is that pause
// end at "bottom 84%" (not 42%) so the ride finishes while the section's
// top — and the "AI Coordinated Pickup" badge pinned there — clears the
// ~115px sticky header with real margin, instead of the finish state
// landing scrolled out from under the nav.
const tl = gsap.timeline({
scrollTrigger: { trigger: wrap, start: "top 82%", end: "bottom 84%", scrub: 0.7 },
});
tl.call(() => activate(0), [], 0);
const legs = [
{ from: 0, to: 25, start: IDLE, dur: LEG, hold: 0 },
{ from: 25, to: 50, start: IDLE + LEG, dur: LEG, hold: CP_HOLD },
{ from: 50, to: 75, start: IDLE + LEG * 2, dur: LEG, hold: CP_HOLD },
{ from: 75, to: 100, start: IDLE + LEG * 3, dur: LEG4, hold: CP_HOLD },
];
legs.forEach((leg, idx) => {
const driveStart = leg.start + leg.hold;
const driveDur = leg.dur - leg.hold;
const accelD = driveDur * 0.32, cruiseD = driveDur * 0.36, brakeD = driveDur * 0.32;
const accelEnd = leg.from + (leg.to - leg.from) * 0.28;
const cruiseEnd = leg.from + (leg.to - leg.from) * 0.72;
const arrival = leg.start + leg.dur;
// Depart, cruise, brake — three distinct phases per leg instead of one
// symmetric ease, so the ride reads as accelerate / hold speed / slow
// into the checkpoint rather than one smooth blob of motion.
tl.to(proxy, { v: accelEnd, duration: accelD, ease: "power2.in", onUpdate: () => place(proxy.v) }, driveStart)
.to(proxy, { v: cruiseEnd, duration: cruiseD, ease: "none", onUpdate: () => place(proxy.v) }, driveStart + accelD)
.to(proxy, { v: leg.to, duration: brakeD, ease: "power2.out", onUpdate: () => place(proxy.v) }, driveStart + accelD + cruiseD)
.call(() => activate(idx + 1), [], arrival);
if (idx === 2) {
// OTP Verification arrival (80%).
tl.call(() => showToast("OTP Verified"), [], arrival)
.to(confirm, { opacity: 0, duration: 0.2 }, arrival + 0.25);
}
});
// Exit beat, entirely inside the last 5% of scroll: arrive -> scan ->
// package loaded -> success glow + badge swap -> van fades. The fade is
// a normal tween on this SAME scrubbed timeline (not a one-shot
// gsap.to() outside it), so scrolling back up smoothly un-fades the
// van instead of leaving it permanently hidden.
const exitStart = IDLE + LEG * 3 + LEG4; // 9.5 -> 95%, matches the last leg's own arrival time
tl.call(() => showToast("Scanning package…"), [], exitStart)
.call(() => showToast("Package Loaded"), [], exitStart + 0.15)
.to(confirm, { opacity: 0, duration: 0.15 }, exitStart + 0.3)
.call(finish, [], exitStart + 0.3)
.to(van, { opacity: 0, duration: EXIT - 0.3, ease: "power1.in" }, exitStart + 0.3);
tl.to(img, { scale: 1.07, ease: "none", duration: tl.duration() }, 0);
return () => {
tl.scrollTrigger?.kill();
tl.kill();
};
}, [stops.length]);
return (
Smart pickup management>}
lede="Every pickup request is received, validated, assigned and confirmed before a single mile is driven — so deliveries start the right way, every time."
/>
{/* Static left/top (matching place(0) in the effect below) so the
van renders parked at Pickup Request in the server-rendered
markup itself — visible on first paint, not just once JS
hydrates and the effect runs a moment later. */}
);
}
/* ============================================================
SECTION 03 — AI Vehicle Selection (split layout)
Richer vehicle illustrations. Hovering an order profile triggers a
brief "AI evaluating" scan sweep before the vehicle reselects.
============================================================ */
/** Rejection reason shown on each non-winning vehicle card, indexed
* [profileIndex][vehicleIndex] — "" for whichever vehicle that profile
* actually selects (never read, since that card gets .is-winner instead). */
const VEH_REJECT_REASONS: string[][] = [
["", "Not Needed", "Too Large", "Too Large", "Too Large"], // Small Parcel -> Bike
["Too Small", "Capacity", "", "Overkill", "Too Large"], // Retail Restock -> Mini Truck
["Too Small", "Capacity", "Capacity", "", "Overkill"], // Bulk Pallet -> Van
["Range Limit", "Range Limit", "Capacity", "Capacity", ""], // Long Haul -> Truck
];
const VEH_DECISION_STEPS = ["Analysing", "Comparing", "Filtering", "Selecting", "Dispatch Ready"];
const AI_VEHICLE_SELECTION_ITEMS = [
{ title: "Small Parcel", text: "Instantly routed to agile urban two-wheeler & EV bike fleets for zero-delay last-mile delivery. (2 kg · 4 km)" },
{ title: "Retail Restock", text: "Optimized for mid-capacity electric vans and urban cargo units with strict SLA time windows. (80 kg · 12 km)" },
{ title: "Bulk Pallet", text: "Matched to heavy-duty mini trucks and multi-pallet freight vehicles with route weight clearance. (600 kg · 30 km)" },
{ title: "Long Haul", text: "Assigned to primary long-distance transport trucks with real-time driver & route monitoring. (2 t · 140 km)" },
];
function Sec03() {
return (
);
}
/* ============================================================
SECTION 04 — Smart Route Intelligence (AI Decision Engine)
A complete replacement for the earlier map/vehicle visualization. The
real value of the product is the AI *making the decision*, not a truck
driving — so this is an enterprise decision dashboard, not navigation.
Four candidate routes appear as glass cards; the AI scores them on live
metrics (ETA / distance / fuel / traffic) with values counting in and
bars filling, then the losers mute and one card wins, a "Best Route
Selected" confirmation lands, and a small dispatch beat runs. Self-
looping ~9s real-time timeline (the model this section has always used),
started once on scroll-in.
============================================================ */
type AdeRoute = { id: string; name: string; eta: number; dist: number; fuel: number; traffic: "Light" | "Moderate" | "Heavy"; score: number; reason: string | null; winner?: boolean };
/** Deterministic candidates — the winner (D) is genuinely best on every
* axis (lowest ETA, shortest distance, least fuel, light traffic, top
* score) so the AI's choice reads as obviously correct, not arbitrary. */
const ADE_ROUTES: AdeRoute[] = [
{ id: "A", name: "Route A", eta: 22, dist: 19.8, fuel: 71, traffic: "Heavy", score: 64, reason: "Heavy traffic" },
{ id: "B", name: "Route B", eta: 18, dist: 22.4, fuel: 66, traffic: "Moderate", score: 73, reason: "Longer distance" },
{ id: "C", name: "Route C", eta: 16, dist: 17.1, fuel: 70, traffic: "Light", score: 81, reason: "Higher fuel cost" },
{ id: "D", name: "Route D", eta: 13, dist: 15.6, fuel: 52, traffic: "Light", score: 97, reason: null, winner: true },
];
const ADE_WINNER = ADE_ROUTES.findIndex((r) => r.winner);
function Sec04() {
const panelRef = useRef(null);
const statusRef = useRef(null);
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
const flowRef = useRef(null);
const resultRef = useRef(null);
const dispatchRef = useRef(null);
const dispatchBarRef = useRef(null);
useEffect(() => {
const panel = panelRef.current;
const status = statusRef.current;
const cards = cardRefs.current.filter(Boolean) as HTMLDivElement[];
const flow = flowRef.current;
const result = resultRef.current;
const dispatch = dispatchRef.current;
const dispatchBar = dispatchBarRef.current;
if (!panel || !status || cards.length === 0 || !flow || !result || !dispatch || !dispatchBar) return;
// Per-card element handles — queried once (same technique the old
// section used with the van's wheels) so the timeline can drive each
// card's numbers/bars without a giant ref matrix.
const parts = cards.map((card) => ({
card,
eta: card.querySelector('[data-k="eta"]')!,
dist: card.querySelector('[data-k="dist"]')!,
fuel: card.querySelector('[data-k="fuel"]')!,
fuelBar: card.querySelector(".sw-ade-fuel-bar > i")!,
scoreVal: card.querySelector(".sw-ade-score-val")!,
scoreBar: card.querySelector(".sw-ade-score-bar > i")!,
traffic: card.querySelector(".sw-ade-traffic")!,
verdict: card.querySelector(".sw-ade-verdict")!,
}));
const blankNums = () => {
parts.forEach((p) => { p.eta.textContent = "—"; p.dist.textContent = "—"; p.fuel.textContent = "—"; p.scoreVal.textContent = "0"; });
};
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
if (reduced) {
gsap.set(cards, { opacity: 1, y: 0 });
parts.forEach((p, i) => {
const r = ADE_ROUTES[i];
p.eta.textContent = `${r.eta}`; p.dist.textContent = r.dist.toFixed(1); p.fuel.textContent = `${r.fuel}`; p.scoreVal.textContent = `${r.score}`;
gsap.set(p.fuelBar, { width: `${r.fuel}%` }); gsap.set(p.scoreBar, { width: `${r.score}%` });
gsap.set([p.traffic, p.verdict], { opacity: 1, y: 0 });
p.card.classList.toggle("is-winner", i === ADE_WINNER);
p.card.classList.toggle("is-muted", i !== ADE_WINNER);
});
gsap.set([flow, result, dispatch], { opacity: 1, y: 0 });
gsap.set(dispatchBar, { width: "100%" });
status.textContent = "Best route selected";
return;
}
const STATUS: [number, string][] = [
[0, "Initializing decision engine…"],
[0.6, "Generating route options…"],
[1.9, "Reading live traffic & road data…"],
[3.0, "Calculating fuel, distance & ETA…"],
[4.2, "Scoring 4 candidate routes…"],
[5.6, "Comparing against optimal thresholds…"],
[6.6, "Best route selected"],
[7.6, "Dispatching vehicle…"],
[8.9, "Shipment in transit"],
];
const tl = gsap.timeline({ paused: true, repeat: -1, repeatDelay: 1.1 });
tl.eventCallback("onUpdate", () => {
const t = tl.time();
let s = STATUS[0][1];
for (const [time, text] of STATUS) { if (t >= time) s = text; else break; }
status.textContent = s;
// "AI is reading this card" glow only during the scoring window.
const scoring = t >= 1.9 && t < 5.5;
cards.forEach((c) => c.classList.toggle("is-scoring", scoring));
});
// Count-up helper — adds a proxy tween straight to the timeline (fromTo
// so every loop restarts cleanly from 0), writing the rounded value to
// the element each frame.
const countTo = (el: HTMLElement, to: number, at: number, dur: number, dec = 0) => {
const o = { v: 0 };
tl.fromTo(o, { v: 0 }, { v: to, duration: dur, ease: "power2.out", onUpdate: () => { el.textContent = dec ? o.v.toFixed(dec) : `${Math.round(o.v)}`; } }, at);
};
// t=0 reset (re-runs every loop): clear classes + blank the numbers so
// Step 1 always begins from a clean slate.
tl.call(() => { cards.forEach((c) => c.classList.remove("is-scoring", "is-muted", "is-winner")); blankNums(); }, [], 0);
// Step 1 — the four route options appear.
tl.fromTo(cards, { opacity: 0, y: 16 }, { opacity: 1, y: 0, duration: 0.5, stagger: 0.12, ease: "power3.out" }, 0.5);
// Step 2 — the AI evaluates each route: metrics count in, bars fill,
// score climbs, traffic pill lands.
parts.forEach((p, i) => {
const r = ADE_ROUTES[i];
const at = 1.9 + i * 0.22;
tl.fromTo(p.traffic, { opacity: 0 }, { opacity: 1, duration: 0.3 }, at);
countTo(p.eta, r.eta, at, 1.0);
countTo(p.dist, r.dist, at, 1.0, 1);
countTo(p.fuel, r.fuel, at, 1.0);
tl.fromTo(p.fuelBar, { width: "0%" }, { width: `${r.fuel}%`, duration: 1.0, ease: "power2.out" }, at);
countTo(p.scoreVal, r.score, at + 0.3, 1.3);
tl.fromTo(p.scoreBar, { width: "0%" }, { width: `${r.score}%`, duration: 1.3, ease: "power2.out" }, at + 0.3);
});
// Flow connector fades in as the compare begins.
tl.fromTo(flow, { opacity: 0 }, { opacity: 1, duration: 0.4 }, 5.3);
// Step 3 — compare: losers mute, the winner highlights, verdicts land.
tl.call(() => {
cards.forEach((c, i) => { c.classList.toggle("is-winner", i === ADE_WINNER); c.classList.toggle("is-muted", i !== ADE_WINNER); });
}, [], 5.6);
parts.forEach((p, i) => { tl.fromTo(p.verdict, { opacity: 0, y: 4 }, { opacity: 1, y: 0, duration: 0.4, ease: "power2.out" }, 5.7 + i * 0.1); });
// Step 4 — the confirmation banner.
tl.fromTo(result, { opacity: 0, y: 12 }, { opacity: 1, y: 0, duration: 0.5, ease: "back.out(1.4)" }, 6.6);
// Step 5 — dispatch: truck + "Delivery Started" + a progress bar.
tl.fromTo(dispatch, { opacity: 0, y: 10 }, { opacity: 1, y: 0, duration: 0.45, ease: "power2.out" }, 7.6);
tl.fromTo(dispatchBar, { width: "0%" }, { width: "100%", duration: 1.4, ease: "power2.inOut" }, 7.8);
const st = ScrollTrigger.create({ trigger: panel, start: "top 80%", once: true, onEnter: () => tl.play(0) });
return () => { st.kill(); tl.kill(); };
}, []);
return (
/ Smart Route Intelligence /
Smart Route Intelligence
Parallel Evaluation & Instant AI Dispatch
Before a single dispatch is made, Doormile generates every possible route and lets the AI decide in real time.
Scoring each option on live traffic, fuel, distance, and ETA guarantees every order is committed to the single path that wins.
Simulating Hundreds of Parallel Possibilities in Real Time
Traditional systems calculate one static route and dispatch it sequentially. Doormile Quantum evaluates hundreds of route possibilities simultaneously in milliseconds.
Simulating every traffic, SLA, and fuel cost outcome in parallel guarantees every order is auto-dispatched along the single most efficient path.
);
}
/* ---------------------------------------------------------------
Phone chrome: a real iOS-style status bar (live JS clock, static
signal/5G/Wi-Fi/battery icons) and a glass notification banner.
Purely presentational additions to the Sec06 mockup — they don't
touch the map SVG or its native marker animation.
--------------------------------------------------------------- */
function PhoneStatusBar() {
// Rendered empty on the server / first paint so client and server markup
// agree, then filled in and re-synced on each minute boundary.
const [time, setTime] = useState("");
useEffect(() => {
const fmt = () => {
const d = new Date();
let h = d.getHours() % 12;
if (h === 0) h = 12;
return `${h}:${d.getMinutes().toString().padStart(2, "0")}`;
};
setTime(fmt());
let interval: ReturnType | undefined;
const now = new Date();
const msToNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds();
const timeout = setTimeout(() => {
setTime(fmt());
interval = setInterval(() => setTime(fmt()), 60000);
}, msToNextMinute);
return () => {
clearTimeout(timeout);
if (interval) clearInterval(interval);
};
}, []);
return (
),
title: "Shipment Journey",
desc: "Track every milestone — from pickup request and fleet driver assignment to OTP verification and successful package collection — with complete real-time visibility.",
},
{
art: ,
title: "Shared Visibility",
desc: "Merchants, fleet drivers, operations teams and clients all watch the same shipment journey from a single source of truth.",
},
{
art: ,
title: "Proof of Delivery",
desc: "Secure OTP and photo confirmation close every shipment at the doorstep, giving tamper-proof evidence the parcel arrived.",
},
];
return (
/ Live Logistics Intelligence /
{headingLines.map((line, i) => (
{line}
))}
Every stakeholder sees the same shipment journey in real time — from dispatch to
doorstep — with live tracking, intelligent updates and complete operational visibility.
MileTruth™AI>}
lede="MileTruth continuously analyzes every completed shipment — turning raw operational data into recommendations that make the next mile faster, cheaper and more reliable."
className="sw-head--split"
/>