# Delivery Tracker

> Live order tracking on a hand-drawn city map: the courier rides the route as the ETA counts down, you can zoom in to follow them, message with quick replies, and rate and tip once it lands.

- Page: https://devl.dev/screens/delivery-tracker
- Kind: screen · Commerce
- Install: `npx shadcn@latest add https://devl.dev/r/delivery-tracker.json`
- Raw source: https://devl.dev/raw/delivery-tracker.tsx
- npm: lucide-react, motion
- coss ui: avatar, button, input, menu, textarea
- License: MIT. The coss ui components it uses (https://coss.com/ui) are AGPL-3.0.

## Usage

```tsx
import { DeliveryTracker } from "@/components/delivery-tracker";

export default function DeliveryTrackerDemo() {
  return (
    <DeliveryTracker
      address="Flat 3, 18 Alder Row"
      courier={{ name: "Tomás Reyes", rating: 4.9, trips: 1284, vehicle: "Green e-bike", phone: "+15555550142" }}
      orderId="LC-20931"
      routeMinutes={18}
      simulateSeconds={40}
      startAt={0.3}
      storeName="Larder & Co."
    />
  );
}
```

## Props

| Prop | Type | Default |
| --- | --- | --- |
| `className`? | `string` |  |
| `storeName`? | `string` | `"Larder & Co."` |
| `orderId`? | `string` | `"LC-20931"` |
| `address`? | `string` | `"Flat 3, 18 Alder Row"` |
| `courier`? | `DeliveryCourier` | `defaultCourier` |
| `items`? | `DeliveryItem[]` | `defaultItems` |
| `routeMinutes`? | `number` | `18` |
| `startAt`? | `number` | `0.3` |
| `simulateSeconds`? | `number` | `40` |
| `now`? | `string` | `"19:24"` |
| `onBack`? | `() => void` |  |

## Source: components/delivery-tracker.tsx

```tsx
"use client";

import {
  BikeIcon,
  CheckIcon,
  ChevronLeftIcon,
  CircleHelpIcon,
  HouseIcon,
  MessageCircleIcon,
  MinusIcon,
  PencilIcon,
  PhoneIcon,
  PlusIcon,
  RotateCcwIcon,
  SendIcon,
  StarIcon,
  StoreIcon,
  XCircleIcon,
} from "lucide-react";
import {
  AnimatePresence,
  animate,
  motion,
  useMotionValue,
  useMotionValueEvent,
  useReducedMotion,
  useTransform,
} from "motion/react";
import { type FormEvent, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Menu, MenuItem, MenuLinkItem, MenuPopup, MenuSeparator, MenuTrigger } from "@/components/ui/menu";
import { Textarea } from "@/components/ui/textarea";

export interface DeliveryItem {
  id: string;
  name: string;
  detail: string;
  price: number;
  qty: number;
  art: "bread" | "milk" | "fruit" | "flowers";
}

export interface DeliveryCourier {
  name: string;
  rating: number;
  trips: number;
  vehicle: string;
  phone: string;
}

export interface DeliveryTrackerProps {
  className?: string;
  storeName?: string;
  orderId?: string;
  address?: string;
  courier?: DeliveryCourier;
  items?: DeliveryItem[];
  routeMinutes?: number;
  startAt?: number;
  simulateSeconds?: number;
  now?: string;
  onBack?: () => void;
}

const spring = { type: "spring", stiffness: 520, damping: 44, mass: 1 } as const;
const easeOut = [0.23, 1, 0.32, 1] as const;
const press = "transition-[color,background-color,box-shadow,scale,opacity] duration-150 ease-out active:scale-[0.97]";

const defaultCourier: DeliveryCourier = {
  name: "Tomás Reyes",
  rating: 4.9,
  trips: 1284,
  vehicle: "Green e-bike",
  phone: "+15555550142",
};

const defaultItems: DeliveryItem[] = [
  { id: "d1", name: "Country sourdough", detail: "800 g, sliced", price: 7.5, qty: 1, art: "bread" },
  { id: "d2", name: "Barista oat milk", detail: "1 L", price: 3.2, qty: 2, art: "milk" },
  { id: "d3", name: "Clementines", detail: "1 kg net", price: 4.8, qty: 1, art: "fruit" },
  { id: "d4", name: "Seasonal stems", detail: "Small bunch", price: 12, qty: 1, art: "flowers" },
];

const W = 800;
const H = 600;
const ROUTE = "M180 500 H384 Q400 500 400 484 V376 Q400 360 416 360 H544 Q560 360 560 344 V236 Q560 220 576 220 H624 Q640 220 640 204 V150";
const START = { x: 180, y: 500 };
const END = { x: 640, y: 150 };
const xs = [0, 100, 240, 400, 560, 720, 800];
const ys = [0, 90, 220, 360, 500, 600];

const stages = ["Confirmed", "Packed", "On the way", "Delivered"] as const;

function phaseText(p: number, first: string) {
  if (p >= 1) return `${first} is at your door`;
  if (p > 0.93) return `${first} is pulling up outside`;
  if (p > 0.8) return `${first} is turning onto Alder Row`;
  if (p > 0.62) return `${first} is crossing Calder Bridge`;
  if (p > 0.45) return `${first} is riding along Quay Road`;
  if (p > 0.27) return `${first} is heading up Linden Avenue`;
  return `${first} is leaving Harbor Street`;
}

function addMinutes(hhmm: string, mins: number) {
  const [h, m] = hhmm.split(":").map(Number);
  const total = h * 60 + m + Math.round(mins);
  const hh = Math.floor(total / 60) % 24;
  const mm = total % 60;
  const suffix = hh >= 12 ? "pm" : "am";
  return `${hh % 12 || 12}:${String(mm).padStart(2, "0")} ${suffix}`;
}

function initials(name: string) {
  return name
    .split(/\s+/)
    .map((p) => p[0])
    .join("")
    .slice(0, 2)
    .toUpperCase();
}

function ItemArt({ art }: { art: DeliveryItem["art"] }) {
  const tile = {
    bread: "bg-amber-500/14 dark:bg-amber-400/12",
    milk: "bg-sky-500/12 dark:bg-sky-400/12",
    fruit: "bg-orange-500/14 dark:bg-orange-400/12",
    flowers: "bg-rose-500/12 dark:bg-rose-400/12",
  }[art];
  return (
    <span aria-hidden className={cn("flex size-10 shrink-0 items-center justify-center rounded-lg", tile)}>
      <svg className="size-6" viewBox="0 0 32 32">
        {art === "bread" && (
          <>
            <path className="fill-amber-600 dark:fill-amber-500" d="M5 18c0-6 5-10 11-10s11 4 11 10v4a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2z" />
            <path className="fill-none stroke-amber-200/80" d="M11 13l2 3M16 12l1 3.5M21 13l-1 3" strokeLinecap="round" strokeWidth="1.5" />
          </>
        )}
        {art === "milk" && (
          <>
            <path className="fill-white dark:fill-sky-100" d="M11 9h10v4l2 3v11a1 1 0 0 1-1 1H10a1 1 0 0 1-1-1V16l2-3z" />
            <rect className="fill-sky-500 dark:fill-sky-400" height="5" rx="1" width="14" x="9" y="18" />
            <rect className="fill-sky-700/70 dark:fill-sky-600" height="3" rx="0.5" width="10" x="11" y="5" />
          </>
        )}
        {art === "fruit" && (
          <>
            <circle className="fill-orange-500 dark:fill-orange-400" cx="12" cy="19" r="6.5" />
            <circle className="fill-orange-400 dark:fill-orange-300" cx="20.5" cy="17" r="6" />
            <path className="fill-emerald-600 dark:fill-emerald-500" d="M20 11c1-3 4-4 6-3-1 2-3 3.5-6 3z" />
          </>
        )}
        {art === "flowers" && (
          <>
            <path className="fill-none stroke-emerald-600 dark:stroke-emerald-500" d="M16 28V15M16 22l-5-6M16 20l5-5" strokeLinecap="round" strokeWidth="1.6" />
            <circle className="fill-rose-500 dark:fill-rose-400" cx="16" cy="10" r="4" />
            <circle className="fill-rose-400 dark:fill-rose-300" cx="10" cy="14" r="3" />
            <circle className="fill-amber-400 dark:fill-amber-300" cx="22" cy="13" r="3" />
          </>
        )}
      </svg>
    </span>
  );
}

function ItemArtStore() {
  return (
    <span aria-hidden className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
      <StoreIcon className="size-4" />
    </span>
  );
}

function Tween({ value }: { value: number }) {
  const reduce = useReducedMotion();
  const mv = useMotionValue(value);
  const text = useTransform(mv, (n) => String(Math.round(n)));
  useEffect(() => {
    if (reduce) {
      mv.set(value);
      return;
    }
    const controls = animate(mv, value, { duration: 0.4, ease: easeOut });
    return () => controls.stop();
  }, [value, reduce, mv]);
  return <motion.span className="tabular-nums">{text}</motion.span>;
}

const ZOOMS = [1, 1.5, 2.2];

export function DeliveryTracker({
  className,
  storeName = "Larder & Co.",
  orderId = "LC-20931",
  address = "Flat 3, 18 Alder Row",
  courier = defaultCourier,
  items = defaultItems,
  routeMinutes = 18,
  startAt = 0.3,
  simulateSeconds = 40,
  now = "19:24",
  onBack,
}: DeliveryTrackerProps) {
  const reduce = useReducedMotion();
  const uid = useId().replace(/:/g, "");
  const first = courier.name.split(" ")[0];
  const progress = useMotionValue(startAt);
  const zoomMv = useMotionValue(1);
  const [zoomIdx, setZoomIdx] = useState(0);
  const [minutes, setMinutes] = useState(Math.ceil(routeMinutes * (1 - startAt)));
  const [phase, setPhase] = useState(() => phaseText(startAt, first));
  const [status, setStatus] = useState<"moving" | "delivered">("moving");
  const [run, setRun] = useState(0);
  const [chat, setChat] = useState(false);
  const [rating, setRating] = useState(0);
  const [tip, setTip] = useState<number | null>(null);
  const [note, setNote] = useState("Leave with the concierge if I’m not in.");
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(note);
  const routeRef = useRef<SVGPathElement>(null);
  const cameraRef = useRef<SVGGElement>(null);
  const courierRef = useRef<SVGGElement>(null);
  const pinsRef = useRef<(SVGGElement | null)[]>([]);
  const chatBtnRef = useRef<HTMLButtonElement>(null);
  const svgRef = useRef<SVGSVGElement>(null);
  const renderScale = useRef(1);
  const measured = useRef(false);
  const arrival = addMinutes(now, routeMinutes * (1 - startAt));

  const draw = useCallback(() => {
    const path = routeRef.current;
    if (!path) return;
    const len = path.getTotalLength();
    const pt = path.getPointAtLength(len * Math.min(Math.max(progress.get(), 0), 1));
    const z = zoomMv.get();
    const m = (1.1 / (z * renderScale.current)).toFixed(4);
    let tx = W / 2 - pt.x * z;
    let ty = H / 2 - pt.y * z;
    tx = Math.min(0, Math.max(W - W * z, tx));
    ty = Math.min(0, Math.max(H - H * z, ty));
    cameraRef.current?.setAttribute("transform", `translate(${tx.toFixed(2)} ${ty.toFixed(2)}) scale(${z.toFixed(4)})`);
    courierRef.current?.setAttribute("transform", `translate(${pt.x.toFixed(2)} ${pt.y.toFixed(2)}) scale(${m})`);
    const pins = [START, END];
    pinsRef.current.forEach((g, i) => {
      g?.setAttribute("transform", `translate(${pins[i].x} ${pins[i].y}) scale(${m})`);
    });
  }, [progress, zoomMv]);

  useMotionValueEvent(progress, "change", (p) => {
    draw();
    setMinutes(Math.max(1, Math.ceil(routeMinutes * (1 - p))));
    setPhase(phaseText(p, first));
  });
  useMotionValueEvent(zoomMv, "change", draw);

  useLayoutEffect(() => {
    const el = svgRef.current;
    if (!el) return;
    const ro = new ResizeObserver(([e]) => {
      const { width, height } = e.contentRect;
      renderScale.current = Math.max(width / W, height / H) || 1;
      if (!measured.current) {
        measured.current = true;
        if (renderScale.current < 0.75) {
          zoomMv.set(ZOOMS[1]);
          setZoomIdx(1);
        }
      }
      draw();
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, [draw, zoomMv]);

  useEffect(() => {
    progress.set(startAt);
    setStatus("moving");
    setRating(0);
    setTip(null);
    draw();
    const controls = animate(progress, 1, {
      duration: simulateSeconds,
      ease: "linear",
      onComplete: () => setStatus("delivered"),
    });
    return () => controls.stop();
  }, [run, progress, startAt, simulateSeconds, draw]);

  useEffect(() => {
    const controls = animate(zoomMv, ZOOMS[zoomIdx], reduce ? { duration: 0 } : { type: "spring", stiffness: 260, damping: 34 });
    return () => controls.stop();
  }, [zoomIdx, zoomMv, reduce]);

  const delivered = status === "delivered";
  const stage = delivered ? 3 : 2;
  const itemsTotal = items.reduce((s, i) => s + i.price * i.qty, 0);
  const fee = 2.99;

  return (
    <div
      className={cn(
        "@container relative isolate flex h-[760px] w-full flex-col overflow-hidden bg-background text-foreground text-sm",
        className,
      )}
    >
      <header className="flex h-14 shrink-0 items-center gap-2.5 border-b px-3 @3xl:px-5">
        {onBack ? (
          <Button aria-label="Back to orders" className={press} onClick={onBack} size="icon-sm" variant="ghost">
            <ChevronLeftIcon />
          </Button>
        ) : (
          <ItemArtStore />
        )}
        <div className="flex min-w-0 flex-col">
          <span className="truncate font-medium">Order from {storeName}</span>
          <span className="font-mono text-muted-foreground text-xs">#{orderId}</span>
        </div>
        <Menu>
          <MenuTrigger
            render={<Button aria-label="Help with this order" className={cn("ms-auto", press)} size="sm" variant="ghost" />}
          >
            <CircleHelpIcon /> <span className="hidden @md:inline">Help</span>
          </MenuTrigger>
          <MenuPopup align="end" className="w-60">
            {delivered ? (
              <MenuItem onClick={() => setRun((r) => r + 1)}>
                <RotateCcwIcon /> Watch the delivery again
              </MenuItem>
            ) : (
              <>
                <MenuItem onClick={() => setChat(true)}>
                  <MessageCircleIcon /> Message {first}
                </MenuItem>
                <MenuLinkItem href={`tel:${courier.phone}`}>
                  <PhoneIcon /> Call {first}
                </MenuLinkItem>
                <MenuItem
                  onClick={() => {
                    setDraft(note);
                    setEditing(true);
                  }}
                >
                  <PencilIcon /> Change drop-off note
                </MenuItem>
              </>
            )}
            <MenuSeparator />
            <MenuItem disabled>
              <XCircleIcon /> Cancel order
              <span className="ms-auto text-muted-foreground text-xs">{delivered ? "Delivered" : "On its way"}</span>
            </MenuItem>
          </MenuPopup>
        </Menu>
      </header>

      <div className="flex min-h-0 flex-1 flex-col @3xl:flex-row">
        <div className="relative h-[280px] shrink-0 overflow-hidden bg-muted @3xl:h-auto @3xl:min-w-0 @3xl:flex-1">
          <svg
            aria-label={`Map: ${phase}`}
            className="absolute inset-0 size-full"
            preserveAspectRatio="xMidYMid slice"
            ref={svgRef}
            role="img"
            viewBox={`0 0 ${W} ${H}`}
          >
            <g ref={cameraRef}>
              <rect className="fill-muted" height={H} width={W} />
              {xs.slice(0, -1).flatMap((x0, i) =>
                ys.slice(0, -1).map((y0, j) => {
                  const x1 = xs[i + 1];
                  const y1 = ys[j + 1];
                  const park = i === 2 && j === 1;
                  return (
                    <rect
                      className={park ? "fill-emerald-500/18 dark:fill-emerald-400/10" : "fill-foreground/[0.025]"}
                      height={y1 - y0 - 22}
                      key={`${x0}-${y0}`}
                      rx={park ? 14 : 5}
                      width={x1 - x0 - 22}
                      x={x0 + 11}
                      y={y0 + 11}
                    />
                  );
                }),
              )}
              {[
                [118, 108, 50, 38],
                [178, 108, 44, 60],
                [418, 108, 60, 44],
                [490, 108, 52, 30],
                [578, 238, 40, 40],
                [658, 238, 46, 30],
                [118, 378, 56, 46],
                [258, 378, 70, 36],
                [418, 378, 40, 50],
                [470, 378, 72, 30],
                [578, 378, 60, 44],
                [258, 518, 60, 40],
                [578, 518, 56, 44],
                [418, 518, 44, 50],
                [738, 108, 46, 70],
              ].map(([x, y, w, h]) => (
                <rect className="fill-foreground/[0.045]" height={h} key={`${x}-${y}`} rx={3} width={w} x={x} y={y} />
              ))}
              {[
                [282, 130],
                [310, 150],
                [350, 128],
                [336, 182],
                [270, 180],
                [372, 168],
              ].map(([x, y]) => (
                <circle className="fill-emerald-500/30 dark:fill-emerald-400/20" cx={x} cy={y} key={`${x}-${y}`} r={9} />
              ))}
              <path
                className="fill-none stroke-sky-400/35 dark:stroke-sky-400/18"
                d="M-20 310 C 120 270, 220 330, 360 296 S 600 262, 820 300"
                strokeLinecap="round"
                strokeWidth={34}
              />
              <g className="fill-none stroke-background" strokeLinecap="round">
                {xs.slice(1, -1).map((x) => (
                  <line key={`v${x}`} strokeWidth={x === 400 || x === 560 ? 20 : 13} x1={x} x2={x} y1={-10} y2={H + 10} />
                ))}
                {ys.slice(1, -1).map((y) => (
                  <line key={`h${y}`} strokeWidth={y === 500 || y === 360 || y === 220 ? 20 : 13} x1={-10} x2={W + 10} y1={y} y2={y} />
                ))}
                <line strokeWidth={11} x1={640} x2={640} y1={220} y2={128} />
                <g strokeWidth={6}>
                  <line x1={484} x2={484} y1={90} y2={220} />
                  <line x1={650} x2={650} y1={360} y2={500} />
                  <line x1={200} x2={200} y1={360} y2={500} />
                  <line x1={100} x2={240} y1={446} y2={446} />
                  <line x1={560} x2={720} y1={160} y2={160} />
                  <line x1={240} x2={400} y1={574} y2={574} />
                </g>
              </g>
              <g className="fill-muted-foreground font-medium" fontSize={11} letterSpacing={0.4}>
                <text x={18} y={492}>
                  HARBOR ST
                </text>
                <text transform="translate(392 590) rotate(-90)">LINDEN AVE</text>
                <text x={436} y={212}>
                  ALDER ROW
                </text>
                <text x={420} y={352}>
                  QUAY RD
                </text>
                <text className="fill-emerald-700/70 dark:fill-emerald-300/60" fontSize={10} x={262} y={208}>
                  FERNWOOD PARK
                </text>
                <text className="fill-sky-700/60 italic dark:fill-sky-300/50" fontSize={11} x={60} y={296}>
                  Calder River
                </text>
              </g>
              <path className="fill-none stroke-background" d={ROUTE} strokeLinecap="round" strokeLinejoin="round" strokeWidth={13} />
              <path
                className="fill-none stroke-info"
                d={ROUTE}
                ref={routeRef}
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={6}
              />
              <motion.path
                className="fill-none stroke-muted-foreground"
                d={ROUTE}
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={6}
                style={{ pathLength: progress }}
              />
              <g
                ref={(el) => {
                  pinsRef.current[0] = el;
                }}
                transform={`translate(${START.x} ${START.y})`}
              >
                <circle className="fill-card stroke-border" r={15} strokeWidth={1.5} />
                <StoreIcon className="text-foreground" height={14} width={14} x={-7} y={-7} />
              </g>
              <g
                ref={(el) => {
                  pinsRef.current[1] = el;
                }}
                transform={`translate(${END.x} ${END.y})`}
              >
                {!delivered && (
                  <motion.circle
                    animate={{ scale: [1, 2], opacity: [0.35, 0] }}
                    className="fill-foreground motion-reduce:hidden"
                    r={16}
                    transition={{ duration: 2, repeat: Number.POSITIVE_INFINITY, ease: "easeOut" }}
                  />
                )}
                <circle className="fill-foreground stroke-background" r={16} strokeWidth={3} />
                <HouseIcon className="text-background" height={15} width={15} x={-7.5} y={-7.5} />
              </g>
              <g ref={courierRef}>
                <circle className="fill-info/20" r={24} />
                <circle className="fill-info stroke-background" r={16} strokeWidth={3} />
                <BikeIcon className="text-white" height={16} width={16} x={-8} y={-8} />
              </g>
            </g>
          </svg>

          <div className="pointer-events-none absolute inset-x-3 top-3 flex items-start justify-between gap-2">
            <span className="flex h-7 items-center gap-2 rounded-full border bg-popover/90 px-2.5 text-xs shadow-xs/5 backdrop-blur-sm">
              <span className="relative flex size-2">
                {!delivered && (
                  <span className="absolute inset-0 animate-ping motion-reduce:hidden rounded-full bg-success opacity-60 [animation-duration:1.6s]" />
                )}
                <span className={cn("relative size-2 rounded-full", delivered ? "bg-muted-foreground" : "bg-success")} />
              </span>
              {delivered ? "Delivered" : "Live"}
            </span>
          </div>
          <div className="absolute right-3 bottom-3 flex flex-col overflow-hidden rounded-xl border bg-popover shadow-xs/5">
            <button
              aria-label="Zoom in on courier"
              className="flex size-9 cursor-pointer items-center justify-center text-foreground outline-none transition-colors hover:bg-accent focus-visible:bg-accent disabled:cursor-default disabled:opacity-40"
              disabled={zoomIdx === ZOOMS.length - 1}
              onClick={() => setZoomIdx((z) => Math.min(ZOOMS.length - 1, z + 1))}
              type="button"
            >
              <PlusIcon className="size-4" />
            </button>
            <span className="h-px bg-border" />
            <button
              aria-label="Zoom out"
              className="flex size-9 cursor-pointer items-center justify-center text-foreground outline-none transition-colors hover:bg-accent focus-visible:bg-accent disabled:cursor-default disabled:opacity-40"
              disabled={zoomIdx === 0}
              onClick={() => setZoomIdx((z) => Math.max(0, z - 1))}
              type="button"
            >
              <MinusIcon className="size-4" />
            </button>
          </div>
        </div>

        <div className="relative min-h-0 flex-1 overflow-y-auto border-t @3xl:w-[380px] @3xl:flex-none @3xl:border-t-0 @3xl:border-l">
          <div className="flex flex-col divide-y">
            <section aria-live="polite" className="flex flex-col gap-4 px-5 py-5">
              <AnimatePresence initial={false} mode="popLayout">
                {delivered ? (
                  <motion.div
                    animate={{ opacity: 1, y: 0 }}
                    className="flex flex-col gap-1"
                    initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6 }}
                    key="done"
                    transition={{ duration: 0.25, ease: easeOut }}
                  >
                    <span className="text-muted-foreground text-xs">Delivered at {arrival}</span>
                    <span className="flex items-center gap-2 font-heading font-semibold text-3xl tracking-tight">
                      Enjoy your order
                      <span className="flex size-6 items-center justify-center rounded-full bg-success text-white">
                        <CheckIcon className="size-4" />
                      </span>
                    </span>
                    <span className="text-muted-foreground">{note ? "Left with the concierge." : "Handed over at the door."}</span>
                  </motion.div>
                ) : (
                  <motion.div
                    animate={{ opacity: 1, y: 0 }}
                    className="flex flex-col gap-1"
                    exit={{ opacity: 0, y: -6 }}
                    initial={{ opacity: 0 }}
                    key="eta"
                    transition={{ duration: 0.2, ease: easeOut }}
                  >
                    <span className="text-muted-foreground text-xs">Arriving by {arrival}</span>
                    <span className="font-heading font-semibold text-3xl tracking-tight">
                      <Tween value={minutes} /> min
                    </span>
                    <AnimatePresence initial={false} mode="popLayout">
                      <motion.span
                        animate={{ opacity: 1, filter: "blur(0px)" }}
                        className="text-muted-foreground"
                        exit={{ opacity: 0, filter: "blur(2px)" }}
                        initial={{ opacity: 0, filter: "blur(2px)" }}
                        key={phase}
                        transition={{ duration: 0.2 }}
                      >
                        {phase}
                      </motion.span>
                    </AnimatePresence>
                  </motion.div>
                )}
              </AnimatePresence>
              <ol aria-label="Order progress" className="grid grid-cols-4 gap-1">
                {stages.map((s, i) => (
                  <li className="flex flex-col gap-2" key={s}>
                    <span className="relative h-1 overflow-hidden rounded-full bg-muted">
                      <motion.span
                        animate={{ scaleX: i < stage ? 1 : i === stage ? (delivered ? 1 : 0.6) : 0 }}
                        className={cn("absolute inset-0 origin-left rounded-full", delivered ? "bg-success" : "bg-info")}
                        initial={false}
                        transition={reduce ? { duration: 0 } : { duration: 0.4, ease: easeOut }}
                      />
                    </span>
                    <span
                      aria-current={i === stage ? "step" : undefined}
                      className={cn(
                        "truncate text-xs",
                        i === stage ? "font-medium text-foreground" : i < stage ? "text-foreground" : "text-muted-foreground",
                      )}
                    >
                      {s}
                    </span>
                  </li>
                ))}
              </ol>
            </section>

            <section aria-label="Your courier" className="flex flex-col gap-4 px-5 py-4">
              <div className="flex items-center gap-3">
                <Avatar className="[--avatar-size:--spacing(10)]">
                  <AvatarFallback className="bg-gradient-to-br from-teal-400 to-sky-500 font-semibold text-white">
                    {initials(courier.name)}
                  </AvatarFallback>
                </Avatar>
                <div className="flex min-w-0 flex-1 flex-col gap-0.5">
                  <span className="truncate font-medium">{courier.name}</span>
                  <span className="flex items-center gap-1 truncate text-muted-foreground text-xs">
                    <StarIcon aria-hidden className="size-3 fill-amber-400 text-amber-400" />
                    <span className="text-foreground tabular-nums">{courier.rating.toFixed(1)}</span>
                    <span aria-hidden>·</span>
                    <span className="truncate">{courier.vehicle}</span>
                  </span>
                </div>
                <AnimatePresence initial={false}>
                  {!delivered && (
                    <motion.div
                      animate={{ opacity: 1 }}
                      className="flex shrink-0 gap-2"
                      exit={{ opacity: 0 }}
                      initial={{ opacity: 0 }}
                      transition={{ duration: 0.15 }}
                    >
                      <Button
                        aria-label={`Message ${first}`}
                        className={cn("rounded-full", press)}
                        onClick={() => setChat(true)}
                        ref={chatBtnRef}
                        size="icon"
                        variant="outline"
                      >
                        <MessageCircleIcon />
                      </Button>
                      <Button
                        aria-label={`Call ${first}`}
                        className={cn("rounded-full", press)}
                        render={<a href={`tel:${courier.phone}`} />}
                        size="icon"
                        variant="outline"
                      >
                        <PhoneIcon />
                      </Button>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>
              <AnimatePresence initial={false}>
                {delivered && (
                  <motion.div
                    animate={{ opacity: 1, y: 0 }}
                    className="flex flex-col gap-3 rounded-xl bg-muted/60 p-3"
                    initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6 }}
                    key="rate"
                    transition={{ duration: 0.25, ease: easeOut, delay: 0.1 }}
                  >
                    <div className="flex items-center justify-between gap-2">
                      <span className="font-medium">How was {first}?</span>
                      <div aria-label="Rate your courier" className="-me-1.5 flex" role="radiogroup">
                        {[1, 2, 3, 4, 5].map((n) => (
                          <button
                            aria-checked={rating === n}
                            aria-label={`${n} star${n > 1 ? "s" : ""}`}
                            className={cn(
                              "flex size-8 cursor-pointer items-center justify-center rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring",
                              press,
                            )}
                            key={n}
                            onClick={() => setRating(n)}
                            role="radio"
                            type="button"
                          >
                            <motion.span
                              animate={{ scale: rating >= n ? 1 : 0.9 }}
                              transition={{ type: "spring", stiffness: 600, damping: 20, delay: rating >= n ? (n - 1) * 0.03 : 0 }}
                            >
                              <StarIcon
                                className={cn(
                                  "size-5 transition-colors duration-150",
                                  rating >= n ? "fill-amber-400 text-amber-400" : "text-muted-foreground/60",
                                )}
                              />
                            </motion.span>
                          </button>
                        ))}
                      </div>
                    </div>
                    <div className="flex items-center gap-2">
                      <span className="text-muted-foreground text-xs">Add a tip</span>
                      <div className="ms-auto flex gap-1.5">
                        {[2, 3, 5].map((amt) => (
                          <button
                            aria-pressed={tip === amt}
                            className={cn(
                              "h-8 min-w-12 cursor-pointer rounded-full border px-3 font-medium text-xs tabular-nums outline-none focus-visible:ring-2 focus-visible:ring-ring",
                              press,
                              tip === amt ? "border-primary bg-primary text-primary-foreground" : "bg-background hover:bg-accent",
                            )}
                            key={amt}
                            onClick={() => setTip((t) => (t === amt ? null : amt))}
                            type="button"
                          >
                            ${amt}
                          </button>
                        ))}
                      </div>
                    </div>
                    <AnimatePresence initial={false}>
                      {(rating > 0 || tip) && (
                        <motion.p
                          animate={{ opacity: 1, height: "auto" }}
                          className="overflow-hidden text-success-foreground text-xs"
                          exit={{ opacity: 0, height: 0 }}
                          initial={{ opacity: 0, height: 0 }}
                          transition={{ duration: 0.2, ease: easeOut }}
                        >
                          Thanks. {tip ? `All $${tip} goes to ${first}.` : `${first} will see your rating.`}
                        </motion.p>
                      )}
                    </AnimatePresence>
                  </motion.div>
                )}
              </AnimatePresence>
            </section>

            <section aria-labelledby={`${uid}-drop`} className="flex flex-col gap-1.5 px-5 py-4">
              <div className="flex h-6 items-center justify-between gap-2">
                <h3 className="text-muted-foreground text-xs" id={`${uid}-drop`}>
                  Drop-off
                </h3>
                {!editing && !delivered && (
                  <Button
                    className={cn("-me-2", press)}
                    onClick={() => {
                      setDraft(note);
                      setEditing(true);
                    }}
                    size="xs"
                    variant="ghost"
                  >
                    <PencilIcon /> Edit note
                  </Button>
                )}
              </div>
              <span className="flex items-center gap-2 font-medium">
                <HouseIcon aria-hidden className="size-4 shrink-0 text-muted-foreground" />
                <span className="truncate">{address}</span>
              </span>
              <AnimatePresence initial={false} mode="popLayout">
                {editing ? (
                  <motion.form
                    animate={{ opacity: 1 }}
                    className="mt-1.5 flex flex-col gap-2"
                    exit={{ opacity: 0 }}
                    initial={{ opacity: 0 }}
                    key="edit"
                    onKeyDown={(e) => {
                      if (e.key === "Escape") setEditing(false);
                    }}
                    onSubmit={(e: FormEvent) => {
                      e.preventDefault();
                      setNote(draft.trim());
                      setEditing(false);
                    }}
                    transition={{ duration: 0.15 }}
                  >
                    <Textarea
                      aria-label="Note for the courier"
                      autoFocus
                      maxLength={140}
                      onChange={(e) => setDraft(e.target.value)}
                      placeholder="Gate code, where to leave it…"
                      rows={2}
                      value={draft}
                    />
                    <div className="flex justify-end gap-2">
                      <Button onClick={() => setEditing(false)} size="sm" variant="ghost">
                        Cancel
                      </Button>
                      <Button className={press} size="sm" type="submit">
                        Save note
                      </Button>
                    </div>
                  </motion.form>
                ) : (
                  <motion.p
                    animate={{ opacity: 1 }}
                    className="ps-6 text-muted-foreground text-pretty"
                    initial={{ opacity: 0 }}
                    key="note"
                    transition={{ duration: 0.15 }}
                  >
                    {note ? `“${note}”` : "No note for the courier."}
                  </motion.p>
                )}
              </AnimatePresence>
            </section>

            <section aria-labelledby={`${uid}-items`} className="flex flex-col px-5 py-4">
              <div className="flex items-baseline justify-between gap-2">
                <h3 className="text-muted-foreground text-xs" id={`${uid}-items`}>
                  Your order
                </h3>
                <span className="text-muted-foreground text-xs tabular-nums">
                  {items.reduce((s, i) => s + i.qty, 0)} items · {storeName}
                </span>
              </div>
              <ul className="mt-3 flex flex-col gap-3">
                {items.map((i) => (
                  <li className="flex items-center gap-3" key={i.id}>
                    <ItemArt art={i.art} />
                    <div className="flex min-w-0 flex-1 flex-col">
                      <span className="truncate font-medium">
                        {i.qty > 1 && <span className="me-1 text-muted-foreground tabular-nums">{i.qty}×</span>}
                        {i.name}
                      </span>
                      <span className="truncate text-muted-foreground text-xs">{i.detail}</span>
                    </div>
                    <span className="tabular-nums">${(i.price * i.qty).toFixed(2)}</span>
                  </li>
                ))}
              </ul>
              <dl className="mt-4 flex flex-col gap-1.5 border-t border-dashed pt-3 text-xs">
                <div className="flex justify-between">
                  <dt className="text-muted-foreground">Items</dt>
                  <dd className="tabular-nums">${itemsTotal.toFixed(2)}</dd>
                </div>
                <div className="flex justify-between">
                  <dt className="text-muted-foreground">Delivery</dt>
                  <dd className="tabular-nums">${fee.toFixed(2)}</dd>
                </div>
                <AnimatePresence initial={false}>
                  {tip ? (
                    <motion.div
                      animate={{ opacity: 1, height: "auto" }}
                      className="overflow-hidden"
                      exit={{ opacity: 0, height: 0 }}
                      initial={{ opacity: 0, height: 0 }}
                      transition={{ duration: 0.2, ease: easeOut }}
                    >
                      <div className="flex justify-between">
                        <dt className="text-muted-foreground">Tip for {first}</dt>
                        <dd className="tabular-nums">${tip.toFixed(2)}</dd>
                      </div>
                    </motion.div>
                  ) : null}
                </AnimatePresence>
                <div className="mt-1 flex justify-between font-medium text-sm">
                  <dt>Paid</dt>
                  <dd className="tabular-nums">${(itemsTotal + fee + (tip ?? 0)).toFixed(2)}</dd>
                </div>
              </dl>
            </section>

            {delivered && (
              <div className="flex justify-center px-5 py-3">
                <Button className={press} onClick={() => setRun((r) => r + 1)} size="sm" variant="ghost">
                  <RotateCcwIcon /> Watch the delivery again
                </Button>
              </div>
            )}
          </div>
        </div>
      </div>

      <CourierChat
        courier={courier}
        onClose={() => {
          setChat(false);
          chatBtnRef.current?.focus();
        }}
        open={chat && !delivered}
        storeName={storeName}
      />
    </div>
  );
}

type Msg = { id: number; from: "me" | "them"; text: string; time: string };

function CourierChat({
  open,
  courier,
  storeName,
  onClose,
}: {
  open: boolean;
  courier: DeliveryCourier;
  storeName: string;
  onClose: () => void;
}) {
  const reduce = useReducedMotion();
  const first = courier.name.split(" ")[0];
  const [msgs, setMsgs] = useState<Msg[]>([
    { id: 1, from: "them", text: `Hi! Picked up your order from ${storeName} and I’m on my way.`, time: "7:26 pm" },
  ]);
  const [text, setText] = useState("");
  const [typing, setTyping] = useState(false);
  const timers = useRef<ReturnType<typeof setTimeout>[]>([]);
  const listRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);
  const nextId = useRef(2);

  useEffect(() => () => timers.current.forEach(clearTimeout), []);
  useEffect(() => {
    if (open) {
      const f = setTimeout(() => inputRef.current?.focus(), 80);
      return () => clearTimeout(f);
    }
  }, [open]);
  useEffect(() => {
    const el = listRef.current;
    if (el) el.scrollTo({ top: el.scrollHeight, behavior: reduce ? "auto" : "smooth" });
  }, [msgs, typing, reduce]);

  const reply = (q: string) => {
    const s = q.toLowerCase();
    if (s.includes("door")) return "No problem, I’ll leave it by the door and send a photo.";
    if (s.includes("bell") || s.includes("ring")) return "Will do, I’ll ring when I’m outside.";
    if (s.includes("down") || s.includes("coming")) return "Great, see you downstairs in a few minutes.";
    return "Got it, thanks!";
  };

  const send = (raw: string) => {
    const t = raw.trim();
    if (!t) return;
    setMsgs((m) => [...m, { id: nextId.current++, from: "me", text: t, time: "Now" }]);
    setText("");
    timers.current.push(setTimeout(() => setTyping(true), 500));
    timers.current.push(
      setTimeout(() => {
        setTyping(false);
        setMsgs((m) => [...m, { id: nextId.current++, from: "them", text: reply(t), time: "Now" }]);
      }, 1700),
    );
  };

  const quick = ["Leave it at the door", "Please ring the bell", "I’m coming down"];

  return (
    <AnimatePresence>
      {open && (
        <>
          <motion.div
            animate={{ opacity: 1 }}
            aria-hidden
            className="absolute inset-0 z-40 bg-black/32"
            exit={{ opacity: 0 }}
            initial={{ opacity: 0 }}
            onClick={onClose}
            transition={{ duration: 0.2, ease: easeOut }}
          />
          <motion.div
            animate={{ y: 0 }}
            aria-label={`Chat with ${first}`}
            aria-modal
            className="absolute inset-x-0 top-16 bottom-0 z-50 flex flex-col rounded-t-2xl border-t bg-popover text-popover-foreground shadow-lg/10 @3xl:top-14 @3xl:left-auto @3xl:w-[380px] @3xl:rounded-none @3xl:border-t-0 @3xl:border-l"
            exit={reduce ? { opacity: 0 } : { y: "100%" }}
            initial={reduce ? { opacity: 0 } : { y: "100%" }}
            onKeyDown={(e) => {
              if (e.key === "Escape") onClose();
            }}
            role="dialog"
            transition={reduce ? { duration: 0.15 } : { type: "spring", stiffness: 440, damping: 42 }}
          >
            <div className="flex items-center gap-3 border-b px-4 py-3">
              <Avatar className="[--avatar-size:--spacing(8)]">
                <AvatarFallback className="bg-gradient-to-br from-teal-400 to-sky-500 font-semibold text-white text-xs">
                  {initials(courier.name)}
                </AvatarFallback>
              </Avatar>
              <div className="flex min-w-0 flex-1 flex-col">
                <span className="truncate font-medium">{first}</span>
                <span className="text-muted-foreground text-xs">Your courier · replies fast</span>
              </div>
              <Button onClick={onClose} size="sm" variant="ghost">
                Done
              </Button>
            </div>
            <div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto px-4 py-4" ref={listRef}>
              <p className="mb-1 text-center text-muted-foreground text-xs">Messages go through the app. Your number stays private.</p>
              <AnimatePresence initial={false}>
                {msgs.map((m) => (
                  <motion.div
                    animate={{ opacity: 1, y: 0, scale: 1 }}
                    className={cn("flex max-w-[80%] flex-col gap-0.5", m.from === "me" ? "items-end self-end" : "items-start self-start")}
                    initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6, scale: 0.98 }}
                    key={m.id}
                    style={{ transformOrigin: m.from === "me" ? "100% 100%" : "0% 100%" }}
                    transition={spring}
                  >
                    <span
                      className={cn(
                        "rounded-2xl px-3 py-2 text-pretty",
                        m.from === "me" ? "rounded-br-md bg-primary text-primary-foreground" : "rounded-bl-md bg-muted",
                      )}
                    >
                      {m.text}
                    </span>
                    <span className="px-1 text-[11px] text-muted-foreground">{m.time}</span>
                  </motion.div>
                ))}
                {typing && (
                  <motion.div
                    animate={{ opacity: 1 }}
                    aria-label={`${first} is typing`}
                    className="flex h-9 w-14 items-center justify-center gap-1 self-start rounded-2xl rounded-bl-md bg-muted"
                    exit={{ opacity: 0 }}
                    initial={{ opacity: 0 }}
                    key="typing"
                    role="status"
                  >
                    {[0, 1, 2].map((i) => (
                      <motion.span
                        animate={reduce ? undefined : { opacity: [0.3, 1, 0.3] }}
                        className="size-1.5 rounded-full bg-muted-foreground"
                        key={i}
                        transition={{ duration: 0.9, repeat: Number.POSITIVE_INFINITY, delay: i * 0.15, ease: "linear" }}
                      />
                    ))}
                  </motion.div>
                )}
              </AnimatePresence>
            </div>
            <div className="flex flex-col gap-2 border-t p-3">
              <div className="-mx-3 flex gap-1.5 overflow-x-auto px-3 [scrollbar-width:none]">
                {quick.map((q) => (
                  <button
                    className={cn(
                      "h-8 shrink-0 cursor-pointer rounded-full border bg-background px-3 text-xs outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring",
                      press,
                    )}
                    key={q}
                    onClick={() => send(q)}
                    type="button"
                  >
                    {q}
                  </button>
                ))}
              </div>
              <form
                className="flex gap-2"
                onSubmit={(e) => {
                  e.preventDefault();
                  send(text);
                }}
              >
                <Input
                  aria-label={`Message ${first}`}
                  className="flex-1"
                  onChange={(e) => setText(e.target.value)}
                  placeholder={`Message ${first}`}
                  ref={inputRef}
                  value={text}
                />
                <Button aria-label="Send" className={press} disabled={!text.trim()} size="icon" type="submit">
                  <SendIcon />
                </Button>
              </form>
            </div>
          </motion.div>
        </>
      )}
    </AnimatePresence>
  );
}
```
