{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"order-tracker","type":"registry:component","title":"Order tracker","description":"Drives a parcel across a street map to your door and stamps each stop as it arrives.","author":"Yash Bavadiya <https://xevrion.dev>","dependencies":["motion"],"registryDependencies":["utils"],"files":[{"path":"components/order-tracker.tsx","type":"registry:component","content":"\"use client\";\n\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport {\n  animate,\n  AnimatePresence,\n  motion,\n  useMotionValue,\n  useTransform,\n} from \"motion/react\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TrackingStop = {\n  label: string;\n  // What the latest scan said, shown while this is the current stop.\n  detail: string;\n  // ISO time the stop was reached. Stops after the current one have none.\n  at?: string;\n};\n\ntype Point = { x: number; y: number };\n\n// The map is drawn 1:1 at the card's full width (540 minus the 8px frame),\n// so strokes and icons land on whole pixels on desktop.\nconst W = 524;\nconst H = 196;\n// Room either side for a centred stop label (88px wide) to stay inside.\nconst INSET = 48;\n// Each stop sits on its own street. Heights alternate so the route reads as\n// a drive across town, never a straight progress bar.\nconst Y = [146, 96, 132, 64, 110];\n// Corner radius where the route turns from one street onto the next.\nconst TURN = 14;\n// Samples per turn for the arc-length table: enough that the parcel stays\n// within a fraction of a pixel of the drawn curve.\nconst TURN_SAMPLES = 10;\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\n// On-screen travel, so ease-in-out. It is the one thing to watch here, a\n// journey between two places, so it runs longer than a UI change.\nconst TRAVEL = { duration: 1.1, ease: [0.77, 0, 0.175, 1] } as const;\nconst ROLL = { type: \"spring\", duration: 0.35, bounce: 0 } as const;\n// Stamps lean like they were pressed by hand, each a little differently.\nconst TILT = [-4, 3, -2, 4, -3];\n\n// Map colours are data, the conventions every street map uses: parks read\n// green and water reads blue. Kept faint so the route stays the focus.\nconst PARK = \"light-dark(oklch(0.93 0.05 150), oklch(0.26 0.035 150))\";\nconst WATER = \"light-dark(oklch(0.91 0.04 235), oklch(0.26 0.04 240))\";\n\nfunction stopPoints(count: number): Point[] {\n  return Array.from({ length: count }, (_, i) => ({\n    x: INSET + ((W - INSET * 2) * i) / Math.max(1, count - 1),\n    y: Y[i % Y.length],\n  }));\n}\n\n// Streets meet at right angles, so each leg runs along the stop's street,\n// turns onto a cross street halfway, then onto the next stop's street.\nfunction corners(stops: Point[]) {\n  const vertices: Point[] = [stops[0]];\n  const stopAt = [0];\n  for (let i = 1; i < stops.length; i++) {\n    const a = stops[i - 1];\n    const b = stops[i];\n    const mid = (a.x + b.x) / 2;\n    if (a.y !== b.y) vertices.push({ x: mid, y: a.y }, { x: mid, y: b.y });\n    stopAt.push(vertices.length);\n    vertices.push(b);\n  }\n  return { vertices, stopAt };\n}\n\n// Rounds every turn with a quadratic curve. Returns the path for drawing\n// and a dense polyline for measuring, both from the same geometry.\nfunction build(stops: Point[]) {\n  const { vertices, stopAt } = corners(stops);\n  const f = (p: Point) => `${p.x.toFixed(1)} ${p.y.toFixed(1)}`;\n  let d = `M${f(vertices[0])}`;\n  const points: Point[] = [vertices[0]];\n  const stopIndex = [0];\n  for (let k = 1; k < vertices.length; k++) {\n    const v = vertices[k];\n    const prev = vertices[k - 1];\n    const next = vertices[k + 1];\n    const isStop = stopAt.includes(k);\n    if (!next || isStop) {\n      d += ` L${f(v)}`;\n      points.push(v);\n      if (isStop) stopIndex.push(points.length - 1);\n      continue;\n    }\n    const inLen = Math.hypot(v.x - prev.x, v.y - prev.y);\n    const outLen = Math.hypot(next.x - v.x, next.y - v.y);\n    const r = Math.min(TURN, inLen / 2, outLen / 2);\n    const a = { x: v.x - ((v.x - prev.x) / inLen) * r, y: v.y - ((v.y - prev.y) / inLen) * r };\n    const b = { x: v.x + ((next.x - v.x) / outLen) * r, y: v.y + ((next.y - v.y) / outLen) * r };\n    d += ` L${f(a)} Q${f(v)} ${f(b)}`;\n    for (let s = 0; s <= TURN_SAMPLES; s++) {\n      const t = s / TURN_SAMPLES;\n      const u = 1 - t;\n      points.push({\n        x: u * u * a.x + 2 * u * t * v.x + t * t * b.x,\n        y: u * u * a.y + 2 * u * t * v.y + t * t * b.y,\n      });\n    }\n  }\n  return { d, vertices, ...measure(points, stopIndex) };\n}\n\n// Arc length along the polyline, pure math, so the server and the client\n// draw the parcel in the same place without measuring the DOM.\nfunction measure(points: Point[], stopIndex: number[]) {\n  const lengths = [0];\n  for (let i = 1; i < points.length; i++) {\n    lengths.push(\n      lengths[i - 1] + Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y),\n    );\n  }\n  const total = lengths[lengths.length - 1] || 1;\n  const pointAt = (fraction: number): Point => {\n    const target = Math.max(0, Math.min(1, fraction)) * total;\n    let lo = 0;\n    let hi = lengths.length - 1;\n    while (hi - lo > 1) {\n      const mid = (lo + hi) >> 1;\n      if (lengths[mid] < target) lo = mid;\n      else hi = mid;\n    }\n    const span = lengths[hi] - lengths[lo] || 1;\n    const t = (target - lengths[lo]) / span;\n    return {\n      x: points[lo].x + (points[hi].x - points[lo].x) * t,\n      y: points[lo].y + (points[hi].y - points[lo].y) * t,\n    };\n  };\n  return { pointAt, stops: stopIndex.map((i) => lengths[i] / total) };\n}\n\nconst DAY = 86_400_000;\n\nexport function OrderTracker({\n  orderId,\n  stops,\n  current,\n  eta,\n  timeZone = \"UTC\",\n  className,\n}: {\n  orderId: string;\n  stops: TrackingStop[];\n  // Index of the latest stop reached.\n  current: number;\n  // ISO time of the promised delivery.\n  eta: string;\n  // Dates print in the carrier's zone, so server and client agree.\n  timeZone?: string;\n  className?: string;\n}) {\n  const reduceMotion = useReducedMotion();\n  const geometry = useMemo(() => {\n    const points = stopPoints(stops.length);\n    return { points, ...build(points) };\n  }, [stops.length]);\n\n  const last = stops.length - 1;\n  const delivered = current >= last;\n\n  // Fraction of the route travelled. The parcel and the solid line both\n  // read it, so they never drift apart.\n  const progress = useMotionValue(geometry.stops[current]);\n  useEffect(() => {\n    const target = geometry.stops[current];\n    if (reduceMotion) {\n      progress.jump(target);\n      return;\n    }\n    const run = animate(progress, target, TRAVEL);\n    return () => run.stop();\n  }, [current, geometry, progress, reduceMotion]);\n  const parcelX = useTransform(progress, (p) => geometry.pointAt(p).x);\n  const parcelY = useTransform(progress, (p) => geometry.pointAt(p).y);\n\n  const format = (iso: string, options: Intl.DateTimeFormatOptions) =>\n    new Intl.DateTimeFormat(\"en-US\", { timeZone, ...options }).format(new Date(iso));\n  // Calendar day number in the carrier's zone, so \"today\" flips at their\n  // midnight rather than at 24 hours.\n  const day = (iso: string) => {\n    const parts = new Intl.DateTimeFormat(\"en-US\", {\n      timeZone,\n      year: \"numeric\",\n      month: \"numeric\",\n      day: \"numeric\",\n    }).formatToParts(new Date(iso));\n    const get = (type: string) => Number(parts.find((p) => p.type === type)?.value);\n    return Date.UTC(get(\"year\"), get(\"month\") - 1, get(\"day\")) / DAY;\n  };\n\n  const reachedAt = stops[current]?.at;\n  const daysLeft = reachedAt ? Math.max(0, day(eta) - day(reachedAt)) : 0;\n  const etaLabel = format(eta, { weekday: \"long\", month: \"short\", day: \"numeric\" });\n\n  const headline = delivered\n    ? \"delivered\"\n    : daysLeft === 0\n      ? \"today\"\n      : \"days\";\n\n  const start = geometry.points[0];\n  const end = geometry.points[last];\n  const streetsY = [...new Set([...geometry.points.map((p) => p.y), 22, 184])];\n  const streetsX = geometry.vertices\n    .filter((v, i, all) => i > 0 && all[i - 1].x === v.x)\n    .map((v) => v.x);\n\n  return (\n    <div\n      className={cn(\n        // 8px frame so the map's 20px corners sit concentric in the 28px card.\n        \"w-[min(540px,100%)] rounded-[28px] bg-background p-2 shadow-raised\",\n        className,\n      )}\n    >\n      <div className=\"px-3 pt-3 pb-4\">\n        <div className=\"flex items-baseline justify-between gap-3\">\n          <p className=\"text-[13px] text-muted-foreground\">Order {orderId}</p>\n          <p className=\"text-[13px] text-muted-foreground tabular-nums\">\n            {delivered && reachedAt\n              ? format(reachedAt, { weekday: \"short\", month: \"short\", day: \"numeric\", hour: \"numeric\", minute: \"2-digit\" })\n              : etaLabel}\n          </p>\n        </div>\n\n        {/* Fixed height, so the map below never moves as the copy changes. */}\n        <div className=\"relative mt-1 h-8 text-2xl font-medium tracking-tight text-foreground\">\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.p\n              key={headline}\n              initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 6, filter: \"blur(4px)\" }}\n              animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n              exit={{ opacity: 0, filter: \"blur(2px)\", transition: { duration: 0.15, ease: EASE_OUT } }}\n              transition={{ duration: 0.25, ease: EASE_OUT }}\n              className=\"absolute inset-x-0 top-0 flex items-baseline gap-[0.25em]\"\n            >\n              {headline === \"delivered\" ? (\n                \"Delivered\"\n              ) : headline === \"today\" ? (\n                \"Arriving today\"\n              ) : (\n                <>\n                  <span>Arriving in</span>\n                  <RollingNumber value={daysLeft} />\n                  <span>{daysLeft === 1 ? \"day\" : \"days\"}</span>\n                </>\n              )}\n            </motion.p>\n          </AnimatePresence>\n          <span className=\"sr-only\">\n            {delivered ? \"Delivered\" : daysLeft === 0 ? \"Arriving today\" : `Arriving in ${daysLeft} ${daysLeft === 1 ? \"day\" : \"days\"}, ${etaLabel}`}\n          </span>\n        </div>\n\n        <div className=\"relative mt-1 h-5 text-sm text-muted-foreground\">\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.p\n              key={current}\n              aria-live=\"polite\"\n              initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 4, filter: \"blur(4px)\" }}\n              animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n              exit={{ opacity: 0, transition: { duration: 0.12 } }}\n              // Waits for the parcel to arrive before the news does.\n              transition={{ duration: 0.25, ease: EASE_OUT, delay: reduceMotion ? 0 : TRAVEL.duration * 0.6 }}\n              className=\"absolute inset-x-0 top-0 truncate\"\n            >\n              {stops[current]?.detail}\n            </motion.p>\n          </AnimatePresence>\n        </div>\n      </div>\n\n      <div className=\"overflow-hidden rounded-[20px] bg-muted\">\n        <svg aria-hidden viewBox={`0 0 ${W} ${H}`} className=\"block w-full\">\n          {/* Town: a river, two parks, then the streets on top of them. */}\n          <path d=\"M-20 70 C 30 52 60 20 110 -20\" fill=\"none\" strokeWidth={22} style={{ stroke: WATER }} />\n          <rect x={180} y={-8} width={96} height={64} rx={10} style={{ fill: PARK }} />\n          <rect x={332} y={146} width={120} height={70} rx={10} style={{ fill: PARK }} />\n          <g className=\"stroke-background\" strokeLinecap=\"round\">\n            {streetsY.map((y) => (\n              <line key={`h${y}`} x1={-10} x2={W + 10} y1={y} y2={y} strokeWidth={y === 22 || y === 184 ? 4 : 9} />\n            ))}\n            {streetsX.map((x) => (\n              <line key={`v${x}`} x1={x} x2={x} y1={-10} y2={H + 10} strokeWidth={9} />\n            ))}\n            {geometry.points.map((p) => (\n              <line key={`m${p.x}`} x1={p.x} x2={p.x} y1={-10} y2={H + 10} strokeWidth={4} />\n            ))}\n          </g>\n\n          {/* The road still to drive, then the part already driven. */}\n          <path d={geometry.d} fill=\"none\" strokeWidth={4} strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"stroke-foreground/20\" />\n          <motion.path\n            d={geometry.d}\n            fill=\"none\"\n            strokeWidth={4}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className=\"stroke-foreground\"\n            style={{ pathLength: progress }}\n          />\n\n          {geometry.points.map((p, i) => {\n            const reached = i <= current;\n            const next = i === current + 1;\n            return (\n              <g key={i} transform={`translate(${p.x} ${p.y})`}>\n                {next && (\n                  <circle\n                    r={8}\n                    className=\"origin-center animate-ping fill-foreground/25 [transform-box:fill-box] motion-reduce:animate-none\"\n                  />\n                )}\n                <circle\n                  r={6}\n                  strokeWidth={2.5}\n                  className={cn(\n                    \"transition-[fill,stroke] duration-200 ease-out\",\n                    reached\n                      ? \"fill-foreground stroke-background\"\n                      : next\n                        ? \"fill-background stroke-foreground\"\n                        : \"fill-background stroke-foreground/30\",\n                  )}\n                />\n              </g>\n            );\n          })}\n\n          {/* Warehouse: flat roofline and a roller door, so it never reads\n              as a second house. The destination is filled, like a map's\n              \"you are here\". */}\n          <Badge x={start.x - 30} y={start.y} label=\"warehouse\">\n            <path d=\"M-7.5 7v-9.5l7.5-4 7.5 4v9.5\" />\n            <path d=\"M-4.5 7v-6h9v6M-4.5 3h9\" />\n          </Badge>\n          <Badge x={end.x + 30} y={end.y} label=\"home\" filled>\n            <path d=\"M-7 0l7-6.5 7 6.5v7h-14z\" />\n            <path d=\"M-2 7v-4.5h4v4.5\" />\n          </Badge>\n\n          <motion.g style={{ x: parcelX, y: parcelY }}>\n            <Pin />\n          </motion.g>\n        </svg>\n      </div>\n\n      <ol className=\"relative mt-3 mb-2 h-[78px]\">\n        {stops.map((stop, i) => {\n          const x = geometry.points[i].x / W;\n          const reached = i <= current && !!stop.at;\n          return (\n            <li\n              key={stop.label}\n              aria-current={i === current ? \"step\" : undefined}\n              style={{ left: `${x * 100}%` }}\n              // INSET keeps a centred label inside the card, down to 375px.\n              className=\"absolute top-0 flex w-[clamp(56px,17%,88px)] -translate-x-1/2 flex-col items-center gap-1.5 text-center\"\n            >\n              <span\n                className={cn(\n                  \"text-[12px] leading-tight font-medium text-balance transition-[color] duration-200 ease-out sm:text-[13px]\",\n                  i <= current ? \"text-foreground\" : \"text-muted-foreground\",\n                )}\n              >\n                {stop.label}\n                <span className=\"sr-only\">\n                  {reached && stop.at\n                    ? `, ${format(stop.at, { month: \"short\", day: \"numeric\", hour: \"numeric\", minute: \"2-digit\" })}`\n                    : i === current + 1\n                      ? \", next\"\n                      : \", pending\"}\n                </span>\n              </span>\n              <AnimatePresence initial={false}>\n                {reached && stop.at && (\n                  <motion.span\n                    key={stop.at}\n                    aria-hidden\n                    // Pressed on like an ink stamp the moment the parcel\n                    // gets there: a touch big, then down onto the paper.\n                    initial={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 1.4 }}\n                    animate={{ opacity: 1, scale: 1 }}\n                    exit={{ opacity: 0, transition: { duration: 0.12, ease: EASE_OUT } }}\n                    transition={{\n                      duration: 0.2,\n                      ease: EASE_OUT,\n                      delay: reduceMotion || i !== current ? 0 : TRAVEL.duration * 0.9,\n                    }}\n                    style={{ rotate: TILT[i % TILT.length] }}\n                    className=\"flex flex-col items-center rounded-[5px] border border-foreground/45 px-1.5 py-0.5 font-mono text-[12px] leading-[15px] whitespace-nowrap text-foreground/75 uppercase tabular-nums\"\n                  >\n                    <span>{format(stop.at, { month: \"short\", day: \"numeric\" })}</span>\n                    <span>{format(stop.at, { hour: \"2-digit\", minute: \"2-digit\", hourCycle: \"h23\" })}</span>\n                  </motion.span>\n                )}\n              </AnimatePresence>\n            </li>\n          );\n        })}\n      </ol>\n    </div>\n  );\n}\n\n// A map pin carrying a parcel, its tip on the road, with a soft contact\n// shadow so it reads as standing on the map rather than pasted over it.\nfunction Pin() {\n  return (\n    <g>\n      <ellipse rx={6} ry={2.5} className=\"fill-foreground/25\" />\n      <path\n        d=\"M0 0C-3 -7 -13 -12 -13 -24A13 13 0 1 1 13 -24C13 -12 3 -7 0 0Z\"\n        strokeWidth={2}\n        className=\"fill-foreground stroke-background\"\n      />\n      <g className=\"fill-none stroke-background\" strokeWidth={1.5} strokeLinejoin=\"round\">\n        <rect x={-6} y={-30} width={12} height={11} rx={1.5} />\n        <path d=\"M-6 -26h12M0 -30v4\" />\n      </g>\n    </g>\n  );\n}\n\n// Where the trip starts and ends, a landmark on the map like a real app's.\nfunction Badge({\n  x,\n  y,\n  label,\n  filled = false,\n  children,\n}: Point & { label: string; filled?: boolean; children: ReactNode }) {\n  return (\n    <g transform={`translate(${x} ${y})`} data-landmark={label}>\n      <circle\n        r={15}\n        strokeWidth={filled ? 2 : 1}\n        className={filled ? \"fill-foreground stroke-background\" : \"fill-background stroke-foreground/15\"}\n      />\n      <g\n        className={cn(\"fill-none\", filled ? \"stroke-background\" : \"stroke-foreground\")}\n        strokeWidth={1.5}\n        strokeLinejoin=\"round\"\n        strokeLinecap=\"round\"\n      >\n        {children}\n      </g>\n    </g>\n  );\n}\n\nfunction RollingNumber({ value }: { value: number }) {\n  const [previous, setPrevious] = useState(value);\n  const [direction, setDirection] = useState(1);\n  if (value !== previous) {\n    setPrevious(value);\n    setDirection(value > previous ? 1 : -1);\n  }\n  const chars = [...String(value)];\n  return (\n    <span aria-hidden className=\"inline-flex tabular-nums\">\n      {chars.map((char, i) => (\n        <Digit key={chars.length - i} char={char} direction={direction} />\n      ))}\n    </span>\n  );\n}\n\nfunction Digit({ char, direction }: { char: string; direction: number }) {\n  const reduceMotion = useReducedMotion();\n  // Counting down, so the new day drops in from above.\n  const offset = (d: number) => (reduceMotion ? \"0%\" : `${d * 100}%`);\n  return (\n    <span className=\"inline-grid overflow-hidden\">\n      <AnimatePresence initial={false} custom={direction}>\n        <motion.span\n          key={char}\n          custom={direction}\n          variants={{\n            enter: (d: number) => ({ y: offset(d), opacity: 0 }),\n            center: { y: \"0%\", opacity: 1 },\n            exit: (d: number) => ({ y: offset(-d), opacity: 0 }),\n          }}\n          initial=\"enter\"\n          animate=\"center\"\n          exit=\"exit\"\n          transition={ROLL}\n          className=\"col-start-1 row-start-1\"\n        >\n          {char}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nconst STOPS: (TrackingStop & { at: string })[] = [\n  { label: \"Ordered\", detail: \"Order confirmed, payment received\", at: \"2026-09-21T09:14:00Z\" },\n  { label: \"Packed\", detail: \"Packed at our Leeds warehouse\", at: \"2026-09-21T16:40:00Z\" },\n  { label: \"Shipped\", detail: \"Handed to Parcelway, tracking PW 4471 2093\", at: \"2026-09-22T08:05:00Z\" },\n  { label: \"Out for delivery\", detail: \"On the van, 14 stops before yours\", at: \"2026-09-24T07:12:00Z\" },\n  { label: \"Delivered\", detail: \"Left with your neighbour at no. 12\", at: \"2026-09-24T13:36:00Z\" },\n];\n\nexport default function OrderTrackerDemo() {\n  const [current, setCurrent] = useState(1);\n  const done = current >= STOPS.length - 1;\n  return (\n    <div className=\"flex w-[min(540px,100%)] flex-col items-center gap-4\">\n      <OrderTracker\n        orderId=\"#48213\"\n        stops={STOPS.map((s, i) => (i <= current ? s : { ...s, at: undefined }))}\n        current={current}\n        eta=\"2026-09-24T20:00:00Z\"\n      />\n      <button\n        type=\"button\"\n        onClick={() => setCurrent((c) => (done ? 0 : c + 1))}\n        className=\"h-10 touch-manipulation rounded-full bg-muted px-4 text-sm font-medium text-foreground shadow-raised outline-hidden transition-[scale] duration-150 ease-out select-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-foreground active:scale-[0.96]\"\n      >\n        {done ? \"Start over\" : \"Simulate next update\"}\n      </button>\n    </div>\n  );\n}\n"},{"path":"hooks/use-reduced-motion.ts","type":"registry:hook","target":"hooks/use-reduced-motion.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n// Motion's useReducedMotion reads the media query on the very first client\n// render, but the server can't know it, so every component that renders\n// differently under reduced motion broke hydration for those users. This\n// reports false during hydration, matching the server HTML, then the real\n// preference straight after, and follows it if it changes.\nconst query = \"(prefers-reduced-motion: reduce)\";\n\nfunction subscribe(onChange: () => void) {\n  const media = window.matchMedia(query);\n  media.addEventListener(\"change\", onChange);\n  return () => media.removeEventListener(\"change\", onChange);\n}\n\nexport function useReducedMotion() {\n  return useSyncExternalStore(\n    subscribe,\n    () => window.matchMedia(query).matches,\n    () => false,\n  );\n}\n"}],"cssVars":{"light":{"shadow-raised":"0 0 0 1px oklch(0 0 0 / 0.06), 0 1px 2px oklch(0 0 0 / 0.06), 0 6px 16px -6px oklch(0 0 0 / 0.12)"},"dark":{"shadow-raised":"0 0 0 1px oklch(1 0 0 / 0.08), 0 1px 2px oklch(0 0 0 / 0.4), 0 6px 16px -6px oklch(0 0 0 / 0.6)"},"theme":{"shadow-raised":"var(--shadow-raised)"}},"categories":["data"],"docs":"From ui lab: https://lab.xevrion.dev/lab/order-tracker"}