Location
Order Tracker
Each stage checks off, the courier rides the route on the map and the time left counts down to delivered.
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { MotionPathPlugin } from "gsap/MotionPathPlugin";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP, MotionPathPlugin);
export type OrderTrackerProps = {
/** Order name or number in the header. */
order?: string;
/** The four stages, in order. */
steps?: [string, string, string, string];
/** Time shown next to each stage. */
times?: [string, string, string, string];
/** Minutes left when the courier leaves; counts down to zero. */
eta?: number;
/** Text before the minutes. */
etaLabel?: string;
/** Word after the minutes. */
minutesLabel?: string;
/** Text that replaces the countdown at the end. */
deliveredLabel?: string;
/** Color of the route, the checks and the courier. */
accent?: string;
/** Replay forever with a pause between runs. */
loop?: boolean;
className?: string;
};
const ROUTE = "M24 92 C 70 92, 72 40, 120 44 S 180 96, 226 70 S 290 24, 316 34";
const CONFETTI = [
{ x: -70, y: -46, r: 140 },
{ x: -42, y: -64, r: -90 },
{ x: -14, y: -58, r: 200 },
{ x: 18, y: -66, r: -160 },
{ x: 46, y: -54, r: 110 },
{ x: 74, y: -40, r: -220 },
{ x: -88, y: -18, r: 80 },
{ x: 92, y: -14, r: -60 },
];
export default function OrderTracker({
order = "Order #2481",
steps = ["Order placed", "Preparing", "On the way", "Delivered"],
times = ["14:02", "14:05", "14:18", "14:30"],
eta = 12,
etaLabel = "Arrives in",
minutesLabel = "min",
deliveredLabel = "Delivered",
accent = "#1f5bff",
loop = true,
className = "",
}: OrderTrackerProps) {
const root = useRef<HTMLDivElement>(null);
const minutes = useRef<HTMLSpanElement>(null);
useGSAP(
() => {
const q = gsap.utils.selector(root);
const left = { v: eta };
const write = () => {
if (minutes.current) minutes.current.textContent = String(Math.ceil(left.v));
};
const route = q("[data-route]")[0] as unknown as SVGPathElement;
const length = route.getTotalLength();
const dots = q("[data-step-dot]");
const checks = q("[data-step-check]");
const labels = q("[data-step-label]");
const done = (i: number, at: number) =>
tl
.to(dots[i], { scale: 1, duration: 0.35, ease: "back.out(2.4)" }, at)
.to(checks[i], { strokeDashoffset: 0, duration: 0.3, ease: "power2.out" }, at + 0.15)
.to(labels[i], { opacity: 1, duration: 0.3 }, at);
const tl = gsap.timeline({ repeat: loop ? -1 : 0, repeatDelay: 1.6 });
tl.set(dots, { scale: 0 })
.set(checks, { strokeDashoffset: 20 })
.set(labels, { opacity: 0.55 })
.set(q("[data-line]"), { scaleY: 0 })
.set(route, { strokeDasharray: length, strokeDashoffset: length })
.set(q("[data-courier]"), { motionPath: { path: route, align: route, alignOrigin: [0.5, 0.5], start: 0, end: 0 } })
.set(q("[data-eta]"), { autoAlpha: 1, y: 0 })
.set(q("[data-delivered]"), { autoAlpha: 0, y: 8 })
.set(q("[data-confetti]"), { x: 0, y: 0, rotation: 0, autoAlpha: 0 })
.set(left, { v: eta })
.add(write, 0);
done(0, 0.3);
tl.to(q("[data-line]"), { scaleY: 1 / 3, duration: 0.5, ease: "power2.inOut" }, 0.8);
done(1, 1.3);
tl.to(q("[data-line]"), { scaleY: 2 / 3, duration: 0.5, ease: "power2.inOut" }, 1.8);
done(2, 2.3);
// A moto anda pela rota e a rota se desenha atrás dela; o tempo restante conta até zero.
tl.to(q("[data-courier]"), { motionPath: { path: route, align: route, alignOrigin: [0.5, 0.5] }, duration: 3, ease: "power1.inOut" }, 2.5)
.to(route, { strokeDashoffset: 0, duration: 3, ease: "power1.inOut" }, 2.5)
.to(left, { v: 0, duration: 3, ease: "power1.inOut", onUpdate: write }, 2.5)
.to(q("[data-line]"), { scaleY: 1, duration: 0.5, ease: "power2.inOut" }, 5.4);
done(3, 5.9);
tl.to(q("[data-eta]"), { autoAlpha: 0, y: -8, duration: 0.25 }, 5.9)
.to(q("[data-delivered]"), { autoAlpha: 1, y: 0, duration: 0.35, ease: "back.out(1.8)" }, 6.0)
.to(q("[data-confetti]"), { autoAlpha: 1, duration: 0.01 }, 6.0)
.to(q("[data-confetti]"), { x: (i) => CONFETTI[i].x, y: (i) => CONFETTI[i].y, rotation: (i) => CONFETTI[i].r, duration: 0.7, ease: "power3.out" }, 6.0)
.to(q("[data-confetti]"), { y: (i) => CONFETTI[i].y + 26, autoAlpha: 0, duration: 0.6, ease: "power1.in" }, 6.7)
.to({}, { duration: 1.2 });
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) tl.progress(1).pause();
},
{ scope: root, dependencies: [eta, loop] },
);
return (
<div ref={root} className={`w-[380px] max-w-full overflow-hidden rounded-2xl bg-white text-neutral-900 shadow-xl ${className}`}>
<div className="relative h-[124px] overflow-hidden bg-[#eef1f5]">
<svg viewBox="0 0 340 124" className="absolute inset-0 h-full w-full" aria-hidden="true">
{/* quarteirões */}
<g fill="#e1e6ee">
<rect x="8" y="8" width="60" height="40" rx="4" />
<rect x="84" y="70" width="70" height="46" rx="4" />
<rect x="150" y="6" width="66" height="34" rx="4" />
<rect x="244" y="82" width="86" height="34" rx="4" />
<rect x="262" y="4" width="70" height="20" rx="4" />
</g>
<path d={ROUTE} fill="none" stroke="#d3dae5" strokeWidth={7} strokeLinecap="round" />
<path data-route d={ROUTE} fill="none" stroke={accent} strokeWidth={4} strokeLinecap="round" />
<circle cx="24" cy="92" r="6" fill="#ffffff" stroke="#1a1d24" strokeWidth={2.5} />
<g transform="translate(316 34)">
<path d="M0 -14 C 7 -14 10 -9 10 -5 C 10 2 0 12 0 12 C 0 12 -10 2 -10 -5 C -10 -9 -7 -14 0 -14 Z" fill="#1a1d24" />
<circle cy="-5" r="3.5" fill="#ffffff" />
</g>
<g data-courier>
<circle r="11" fill={accent} opacity={0.18} />
<circle r="7" fill={accent} stroke="#ffffff" strokeWidth={2.5} />
<path d="M-2.5 -3 L 3 0 L -2.5 3 Z" fill="#ffffff" />
</g>
</svg>
</div>
<div className="p-5">
<div className="flex items-baseline justify-between gap-3">
<p className="text-[13px] font-semibold text-neutral-500">{order}</p>
<div className="relative grid text-right">
<p data-eta className="text-[13px] text-neutral-600 [grid-area:1/1]">
{etaLabel}{" "}
<span ref={minutes} className="font-bold text-neutral-900 tabular-nums">
{eta}
</span>{" "}
{minutesLabel}
</p>
<p data-delivered className="text-[13px] font-bold [grid-area:1/1]" style={{ color: accent }}>
{deliveredLabel}
</p>
<span aria-hidden="true" className="pointer-events-none absolute top-1/2 right-6">
{CONFETTI.map((_, i) => (
<i
key={i}
data-confetti
className="absolute block h-[7px] w-[4px] rounded-[1px]"
style={{ backgroundColor: i % 3 === 0 ? "#f5b400" : i % 3 === 1 ? accent : "#22c55e" }}
/>
))}
</span>
</div>
</div>
<ol className="relative mt-4 space-y-3.5">
<span aria-hidden="true" className="absolute top-2.5 bottom-2.5 left-[10px] w-[2px] bg-neutral-200" />
<span
data-line
aria-hidden="true"
className="absolute top-2.5 bottom-2.5 left-[10px] w-[2px] origin-top"
style={{ backgroundColor: accent }}
/>
{steps.map((step, i) => (
<li key={step} className="relative flex items-center gap-3">
<span className="relative grid h-[22px] w-[22px] shrink-0 place-items-center rounded-full border-2 border-neutral-200 bg-white">
<span data-step-dot className="absolute inset-[-2px] rounded-full" style={{ backgroundColor: accent }} />
<svg viewBox="0 0 24 24" className="relative h-3 w-3" fill="none" stroke="#ffffff" strokeWidth={3.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path data-step-check d="M5 12.5l4.5 4.5L19 7.5" strokeDasharray={20} strokeDashoffset={20} />
</svg>
</span>
<span data-step-label className="flex flex-1 justify-between text-[13.5px]">
<span className="font-semibold">{step}</span>
<span className="text-neutral-500 tabular-nums">{times[i]}</span>
</span>
</li>
))}
</ol>
</div>
</div>
);
}
Paste this into Claude Code inside your project. It installs the skills your agent needs, asks for your texts and colors, and recreates this animation in your stack.
SkillsGSAP skillsHyperFrames · optional, for video
Claude Code
Recreate the "Order Tracker" animated component from Motionshelf (https://motionshelf-pi.vercel.app) in my current project, adapted to my brand. Answer me in the language I write to you in. Work in this order: 1. Tools: Check whether the official GSAP skills are installed (gsap-core, gsap-timeline, gsap-react). If they are not, ask me and then run `npx skills add https://github.com/greensock/gsap-skills` (choose Claude Code). If they do not show up in this session yet, read the SKILL.md files the installer created for gsap-timeline and gsap-react before writing code. 2. Project: Look at my project: framework (Next.js, Vite, plain HTML…), TypeScript or JavaScript, Tailwind or plain CSS. Build it as one component that fits this stack. In React use gsap and @gsap/react; in plain HTML use GSAP from npm or a CDN. 3. Ask me before coding: Ask me these in a single message, showing the example values as defaults: - the four stages and their times (it can also show a service process, not only a delivery) - the order label, the minutes and the texts - the accent color - the language of the texts 4. The animation What it looks like: A white card: on top a small light map with blocks, a gray road from a store dot to a home pin and a round courier marker; below it the order number, "Arrives in 12 min" and four stages in a vertical list (round markers joined by a line, stage name and time). Step by step: - Stages check off at 0.3 s, 1.3 s, 2.3 s and 5.9 s: the marker fills with color (scale 0 → 1, back.out(2.4)), the check draws itself (stroke-dashoffset to 0) and the label goes from 55% to full opacity. Between them the vertical line grows a third at a time (scaleY, 0.5 s). - From 2.5 s to 5.5 s the courier follows the road with MotionPathPlugin (align to the path, power1.inOut), the colored route draws behind it (stroke-dashoffset) and the minutes count down from 12 to 0. - At 5.9 s "Arrives in" is replaced by "Delivered" (back.out(1.8)) and eight small confetti pieces burst from it to fixed offsets, then fall a little and fade by 7.3 s. Rules: - Use one GSAP timeline that loops forever with a short pause between runs (repeat: -1). If the step by step gives the pause, use that value; otherwise repeatDelay about 1.4 s. - Set every starting state with set() at the start of the timeline, never with from(): on the second loop from() starts from the wrong place. - Elements that appear later and would push the layout (new chat bubbles, new rows) stay display:none until their turn; elements that already have their place in the layout (a subtitle, a button) start hidden with autoAlpha 0, so nothing jumps. - If there is a cursor or pointer, it aims at the measured center of its target (getBoundingClientRect), never at fixed pixel coordinates; if the component is scaled, divide by the scale. - Blur only in the middle of a movement and always end at blur(0). - With prefers-reduced-motion, jump to the final frame and stop (tl.progress(1).pause()). - In React, use useGSAP from @gsap/react with a scope ref, so the timeline is cleaned up on unmount. - All example content stays generic (no real brands, logos or people). - Register MotionPathPlugin with gsap.registerPlugin. Confetti offsets are a fixed list, not random, so every loop looks the same. 5. Check: Run it and watch the first and the third loop (they must look the same). Check it at 390 px wide with no sideways scroll, with reduced motion turned on, and with a clean console. Tell me what you checked. 6. Video (optional): If I also want this as an MP4 (ads, stories, WhatsApp status), install the HyperFrames plugin with `claude plugin marketplace add heygen-com/hyperframes` and `claude plugin install hyperframes@hyperframes`, then use /hyperframes:hyperframes to render the same animation in 9:16, 1:1 or 16:9. Rendering needs FFmpeg installed, or render with --docker (downloads about 3.3 GB the first time). Ask me before installing. Shortcut: If I only want the ready-made component instead, it installs with `npx shadcn@latest add https://motionshelf-pi.vercel.app/r/order-tracker.json`.
Other agentsUsing Cursor, Codex, Gemini CLI or another agent? This version tells it to find its own way to load the same skills before it starts.
Other agents
Recreate the "Order Tracker" animated component from Motionshelf (https://motionshelf-pi.vercel.app) in my current project, adapted to my brand. Answer me in the language I write to you in. Work in this order: 1. Tools: These instructions were written for Claude Code, which uses "skills" (folders with a SKILL.md that teach it a technique). First find out how you load skills, rules or instructions, and get the same knowledge: (a) if your agent is supported by the skills installer, ask me and run `npx skills add https://github.com/greensock/gsap-skills --agent <your-agent>` (for example codex, opencode, antigravity); (b) otherwise copy the skill folders from https://github.com/greensock/gsap-skills into your skills or rules directory (in Cursor: Settings → Rules → Remote Rule (GitHub) → greensock/gsap-skills); (c) if you cannot load skills at all, open the SKILL.md files in https://github.com/greensock/gsap-skills and read them as instructions before coding. Tell me which path you used. 2. Project: Look at my project: framework (Next.js, Vite, plain HTML…), TypeScript or JavaScript, Tailwind or plain CSS. Build it as one component that fits this stack. In React use gsap and @gsap/react; in plain HTML use GSAP from npm or a CDN. 3. Ask me before coding: Ask me these in a single message, showing the example values as defaults: - the four stages and their times (it can also show a service process, not only a delivery) - the order label, the minutes and the texts - the accent color - the language of the texts 4. The animation What it looks like: A white card: on top a small light map with blocks, a gray road from a store dot to a home pin and a round courier marker; below it the order number, "Arrives in 12 min" and four stages in a vertical list (round markers joined by a line, stage name and time). Step by step: - Stages check off at 0.3 s, 1.3 s, 2.3 s and 5.9 s: the marker fills with color (scale 0 → 1, back.out(2.4)), the check draws itself (stroke-dashoffset to 0) and the label goes from 55% to full opacity. Between them the vertical line grows a third at a time (scaleY, 0.5 s). - From 2.5 s to 5.5 s the courier follows the road with MotionPathPlugin (align to the path, power1.inOut), the colored route draws behind it (stroke-dashoffset) and the minutes count down from 12 to 0. - At 5.9 s "Arrives in" is replaced by "Delivered" (back.out(1.8)) and eight small confetti pieces burst from it to fixed offsets, then fall a little and fade by 7.3 s. Rules: - Use one GSAP timeline that loops forever with a short pause between runs (repeat: -1). If the step by step gives the pause, use that value; otherwise repeatDelay about 1.4 s. - Set every starting state with set() at the start of the timeline, never with from(): on the second loop from() starts from the wrong place. - Elements that appear later and would push the layout (new chat bubbles, new rows) stay display:none until their turn; elements that already have their place in the layout (a subtitle, a button) start hidden with autoAlpha 0, so nothing jumps. - If there is a cursor or pointer, it aims at the measured center of its target (getBoundingClientRect), never at fixed pixel coordinates; if the component is scaled, divide by the scale. - Blur only in the middle of a movement and always end at blur(0). - With prefers-reduced-motion, jump to the final frame and stop (tl.progress(1).pause()). - In React, use useGSAP from @gsap/react with a scope ref, so the timeline is cleaned up on unmount. - All example content stays generic (no real brands, logos or people). - Register MotionPathPlugin with gsap.registerPlugin. Confetti offsets are a fixed list, not random, so every loop looks the same. 5. Check: Run it and watch the first and the third loop (they must look the same). Check it at 390 px wide with no sideways scroll, with reduced motion turned on, and with a clean console. Tell me what you checked. 6. Video (optional): If I also want this as an MP4 (ads, stories, WhatsApp status), install the HyperFrames skills with `npx skills add heygen-com/hyperframes` (choose your agent) and render the same animation in 9:16, 1:1 or 16:9. Rendering needs FFmpeg installed, or render with --docker (downloads about 3.3 GB the first time). Ask me before installing. Shortcut: If I only want the ready-made component instead, it installs with `npx shadcn@latest add https://motionshelf-pi.vercel.app/r/order-tracker.json`.
Install
npx shadcn@latest add https://motionshelf-pi.vercel.app/r/order-tracker.jsonCopies the file into your project. Requires React 19, Tailwind CSS v4 and GSAP (installed as a dependency).
Props
| Name | Type | Default | Description |
|---|---|---|---|
| order | string | "Order #2481" | Order name or number in the header. |
| steps | [string, string, string, string] | ["Order placed", …] | The four stages, in order. |
| times | [string, string, string, string] | ["14:02", …] | Time next to each stage. |
| eta | number | 12 | Minutes left when the courier leaves. |
| etaLabel | string | "Arrives in" | Text before the minutes. |
| minutesLabel | string | "min" | Word after the minutes. |
| deliveredLabel | string | "Delivered" | Text that replaces the countdown at the end. |
| accent | string | "#1f5bff" | Color of the route, the checks and the courier. |
| loop | boolean | true | Replay forever with a pause between runs. |
| className | string | "" | Extra classes for the outer element. |
Dependencies
- gsap
- @gsap/react