Components

Getting started

Blocks9

Components154

More

An order’s route with a parcel riding it: stops light as it passes, the arrival day rolls, and the carrier’s scans slide in.

Used forOrder pagesAccount ordersFood and groceryPrint and hardware

Install with the shadcn CLI

$ npx shadcn@latest add https://hairlineui.com/r/order-status.json

Templates

Need the whole page?

43 Next.js landing pages, each designed from scratch. Try any of them live; one is $39, all of them $99, and 5 are free.

All 43 templates

Where it’s used

  • Order pages

    The page an order email links to, from placed to delivered.

  • Account orders

    Each order in a customer’s account, with tracking and cancel.

  • Food and grocery

    An order moving from kitchen to door, with a time instead of a date.

  • Print and hardware

    Made-to-order items through making, shipping and delivery.

What it handles for you

An order page is usually a status badge, a progress bar, a tracking link and a list of scans that disagree with each other. This block draws them from one status and one list.

Instead of wiring upan order status badgea shipping progress bara tracking number fielda tracking timelinea cancel order flow
  1. Where it is, at a glance

    Five stops on a route; the parcel sits at the current one and the passed ones are lit.

  2. Moves you can see

    A new status sends the parcel along the route and lights each stop as it passes.

  3. The day, large

    eta reads as the weekday and date; a new date morphs and rolls, delivered reads Delivered.

  4. Tracking in one tap

    Carrier, tracking number with Copy, and a Track link out.

  5. Scans as they come

    events newest first, three until Show all; a new one slides in at the top.

  6. Cancel before it ships

    onCancel shows Cancel order while it’s ordered or packed; it asks once, then Cancelling → Cancelled.

Overview

Most order pages are a status word and a table of scans you have to read bottom up. Here the lit card carries the order’s route, Ordered → Packed → Shipped → Out → Delivered, as a hairline with a parcel riding it. When the status moves on, the parcel travels to the new stop and every stop it passes lights as it goes by; the arrival day is large, and when the date changes the weekday morphs and the day rolls. Under it the tracking number copies in one tap, the carrier’s scans list newest first and a new one slides in at the top, and until the order ships Cancel order asks once and then cancels, dimming the route. The order is yours: pass status, eta and events from your database or the carrier’s webhook.

States

ordered
The lit card with the order number, a status chip, Arriving and the date large, and the route with the parcel at Ordered; Cancel order under the scans.
moving
A new status sends the parcel along the hairline (1100ms) while the lit line follows; each stop lights in turn as the parcel passes and its label brightens. The chip morphs to the new stage.
date
When eta changes the weekday and month morph and the day rolls.
scan
A new event slides in at the top of Updates with the lit dot; older ones move down.
delivered
The parcel at the last stop, every stop lit, the headline Delivered with the day it arrived.
cancelled
Cancel order asks “Yes, cancel this order” (reverts after 4s), then Cancelling → Cancelled; the card loses its colour, the route dims and the date reads Not on its way.

Usage

usage.tsx
import { OrderStatus } from "@/components/order-status";<OrderStatus  number={order.number}  status={order.status} // "ordered" | "packed" | "shipped" | "out" | "delivered" | "cancelled"  eta={order.eta}  carrier={order.carrier}  tracking={order.trackingNumber}  events={order.events}  onCancel={() => api.cancelOrder(order.id)}/>

Depends on lucide-react, installed for you by the CLI along with the keyframes it needs.

Works with

With your own API

Read the order from your server and check again now and then (or push it over a socket); the parcel moves whenever status changes.

example.tsx
"use client";import { useEffect, useState } from "react";import { OrderStatus, type OrderStatusProps } from "@/components/order-status";type Order = Omit<OrderStatusProps, "onCancel">;export function TrackOrder({ id, initial }: { id: string; initial: Order }) {  const [order, setOrder] = useState(initial);  useEffect(() => {    const timer = setInterval(async () => setOrder(await (await fetch(`/api/orders/${id}`)).json()), 30000);    return () => clearInterval(timer);  }, [id]);  return (    <OrderStatus      {...order}      onCancel={async () => {        const res = await fetch(`/api/orders/${id}/cancel`, { method: "POST" });        if (!res.ok) throw new Error((await res.json()).message); // "It's already been packed."      }}    />  );}

Props

PropTypeDescription
numberstringThe order number, shown as “Order #…”.
status"ordered" | "packed" | "shipped" | "out" | "delivered" | "cancelled"Where the order is; a change moves the parcel.
etaDate | string | numberWhen it’s expected, or when it arrived once delivered.
window / addressstring“by 6pm” after the date, and where it’s going.
carrier / tracking / trackingUrlstringThe carrier’s name, the tracking number (with Copy) and a link to their page.
eventsOrderEvent[]Newest first: { id, at, title, place? }.
onCancel() => Promise<unknown>Shows Cancel order while ordered or packed; throw an Error to say why not.
visibleEvents / localenumber / stringdefault 3 / browserScans before Show all, and how dates and times are written.

Accessibility and motion

  • The route is an ordered list with the current stop marked aria-current="step"; each stop says done or current to screen readers.
  • The status chip is a status region, so a new stage is announced once; scans are a plain list.
  • The parcel is a single composited layer moved along the route, never re-laid out per frame.
  • Its light comes from your theme: the card mixes --primary-foreground into --primary, the route is 1px and 2px lines in the same light, and a cancelled order drains its colour. No drop shadows, glow or blur.
  • Built from Copy button, Number roll, Status button and Text morph, which the CLI installs with it. No animation library; with reduced motion the parcel and stops change in place.

Requirements

React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

The CLI also installs Copy button, Number roll, Status button, Text morph.

Source

components/order-status.tsx
"use client";import * as React from "react";import { ArrowUpRight, Package } from "lucide-react";import { CopyButton } from "./copy-button";import { NumberRoll } from "./number-roll";import { StatusButton, type ActionStatus } from "./status-button";import { TextMorph } from "./text-morph";import { useRowMotion } from "./use-row-motion";import { EASE_THROW, INTERACTION_FOCUS, useInteractionReducedMotion } from "./ui-interaction-motion";/* ───────────────────────────────────────────────────────── * ORDER STATUS: the parcel on its route * *   route     the lit card carries the order's route, Ordered → *             Packed → Shipped → Out for delivery → Delivered, as a *             hairline; a parcel rides it to where the order is *             and every stop it passes lights as it goes by *   arriving  the day it arrives is large; when the date moves the *             weekday morphs and the day rolls *   events    the carrier's scans, newest first; a new one slides *             in at the top *   cancel    before it ships, Cancel order asks once, then goes *             Cancelling → Cancelled and the route dims * * The order is yours: pass status, eta and events from your * database or the carrier's webhook (a refetch, a poll or a * socket); onCancel does the cancelling. * ───────────────────────────────────────────────────────── */export type OrderStage = "ordered" | "packed" | "shipped" | "out" | "delivered";export interface OrderEvent {  id: string;  at: Date | string | number;  title: string;  /** Where it happened, e.g. "Leeds depot". */  place?: string;}export interface OrderStatusProps {  /** Shown as "Order #HL-2048". */  number: string;  status: OrderStage | "cancelled";  /** When it's expected; when it arrived once delivered. */  eta?: Date | string | number;  /** e.g. "by 9pm", after the date. */  window?: string;  /** Where it's going, one line. */  address?: string;  carrier?: string;  tracking?: string;  trackingUrl?: string;  /** Newest first. */  events?: OrderEvent[];  /** Offered until the order ships; throw an Error to say why not. */  onCancel?: () => Promise<unknown>;  /** Events shown before Show all. */  visibleEvents?: number;  locale?: string;  className?: string;}const STAGES: { id: OrderStage; label: string; short: string }[] = [  { id: "ordered", label: "Order placed", short: "Ordered" },  { id: "packed", label: "Packed", short: "Packed" },  { id: "shipped", label: "Shipped", short: "Shipped" },  { id: "out", label: "Out for delivery", short: "Out" },  { id: "delivered", label: "Delivered", short: "Delivered" },];const EASE = "cubic-bezier(0.16,1,0.3,1)";const RIDE = 1100;const NONE: OrderEvent[] = [];const subscribeNothing = () => () => {};/** False on the server and while hydrating: text is formatted in one fixed locale until then, so the server's and the visitor's never disagree. */const useHydrated = () => React.useSyncExternalStore(subscribeNothing, () => true, () => false);const mix = (color: string, amount: number, into = "transparent") => `color-mix(in oklab, ${color} ${amount}%, ${into})`;const CARD_LIT: React.CSSProperties = {  backgroundColor: "var(--primary)",  backgroundImage: `radial-gradient(120% 80% at 0% 0%, ${mix("var(--primary-foreground)", 22)} 0%, transparent 55%), linear-gradient(175deg, transparent 40%, ${mix("black", 24)} 100%)`,  boxShadow: `inset 0 1px 0 ${mix("var(--primary-foreground)", 36)}, inset 0 0 0 1px ${mix("var(--primary-foreground)", 12)}`,};const CARD: React.CSSProperties = {  backgroundImage: `linear-gradient(180deg, var(--card), ${mix("var(--card)", 95, "var(--foreground)")})`,  boxShadow: `0 0 0 1px var(--border), inset 0 1px 0 ${mix("var(--background)", 85)}`,};export function OrderStatus({  number,  status,  eta,  window: timeWindow,  address,  carrier,  tracking,  trackingUrl,  events = NONE,  onCancel,  visibleEvents = 3,  locale,  className = "",}: OrderStatusProps) {  const reduced = useInteractionReducedMotion();  // Dates depend on the visitor's locale and time zone, so they're written once the block is in the browser.  const hydrated = useHydrated();  const baseId = React.useId();  const [cancel, setCancel] = React.useState<{ status: ActionStatus; confirm: boolean; error?: string }>({ status: "idle", confirm: false });  const [expanded, setExpanded] = React.useState(false);  const listRef = React.useRef<HTMLOListElement>(null);  const ghostRef = React.useRef<HTMLDivElement>(null);  const cancelled = status === "cancelled" || cancel.status === "success";  const at = Math.max(0, STAGES.findIndex((s) => s.id === status));  const stage = cancelled ? null : STAGES[at];  const progress = at / (STAGES.length - 1);  // Where the parcel set off from, so stops light up as it passes them, not all at once.  const [from, setFrom] = React.useState(at);  const [seenAt, setSeenAt] = React.useState(at);  if (at !== seenAt) {    setFrom(seenAt);    setSeenAt(at);  }  const passDelay = (i: number) => (reduced || at <= from || i <= from ? 0 : ((i - from) / (at - from)) * RIDE * 0.85);  const fmt = React.useMemo(    () => ({      weekday: new Intl.DateTimeFormat(locale, { weekday: "long" }),      month: new Intl.DateTimeFormat(locale, { month: "short" }),      time: new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit" }),      day: new Intl.DateTimeFormat(locale, { day: "numeric", month: "short" }),    }),    [locale],  );  const etaDate = eta !== undefined && hydrated ? new Date(eta) : null;  const rows = expanded ? events : events.slice(0, visibleEvents);  const ranks = React.useMemo(() => new Map(events.map((e, i) => [e.id, i])), [events]);  useRowMotion(listRef, { reduced, ranks, ghosts: ghostRef });  const canCancel = Boolean(onCancel) && (cancel.status === "success" || (!cancelled && (status === "ordered" || status === "packed")));  const cancelOrder = async () => {    if (!onCancel || cancel.status === "pending") return;    if (!cancel.confirm) return setCancel({ status: "idle", confirm: true });    setCancel({ status: "pending", confirm: true });    try {      await onCancel();      setCancel({ status: "success", confirm: true });    } catch (e) {      setCancel({ status: "error", confirm: true, error: e instanceof Error && e.message ? e.message : "Couldn’t cancel. Try again." });    }  };  React.useEffect(() => {    if (!cancel.confirm || cancel.status !== "idle") return;    const timer = window.setTimeout(() => setCancel({ status: "idle", confirm: false }), 4000);    return () => window.clearTimeout(timer);  }, [cancel]);  const headline = cancelled ? "Cancelled" : status === "delivered" ? "Delivered" : "Arriving";  return (    <section aria-labelledby={`${baseId}-title`} className={`w-full max-w-[420px] rounded-[28px] p-2 text-card-foreground ${className}`} style={CARD}>      {/* Its natural width where the parent sizes to its content, without forcing it on narrow screens. */}      <div aria-hidden="true" data-natural-width className="grid h-0" style={{ gridTemplateColumns: "minmax(0, 420px)" }} />      <div className="@container w-full">        {/* The one lit surface: the arrival and the route. */}        <div className="rounded-[22px] p-4 text-primary-foreground" style={{ ...CARD_LIT, filter: cancelled ? "saturate(0.35)" : "none", transition: reduced ? "none" : `filter 500ms ${EASE}` }}>          <div className="flex items-center justify-between gap-3 text-[12.5px]">            <h2 id={`${baseId}-title`} className="font-mono text-primary-foreground/75">              Order #{number}            </h2>            <span className="rounded-full px-2 py-0.5 font-medium" style={{ background: mix("var(--primary-foreground)", 16) }} role="status">              <TextMorph>{cancelled ? "Cancelled" : (stage?.label ?? "")}</TextMorph>            </span>          </div>          <p className="mt-3 text-[12.5px] text-primary-foreground/70">            <TextMorph>{headline}</TextMorph>          </p>          <p className="mt-0.5 flex items-baseline gap-[0.28em] text-[30px] font-medium leading-none tracking-[-0.035em]" suppressHydrationWarning>            {etaDate && !cancelled ? (              <>                <TextMorph>{fmt.weekday.format(etaDate)}</TextMorph>                <span className="tabular-nums">                  <NumberRoll value={etaDate.getDate()} duration={800} />                </span>                <TextMorph className="text-primary-foreground/70">{fmt.month.format(etaDate)}</TextMorph>              </>            ) : (              <span className="text-primary-foreground/60">{cancelled ? "Not on its way" : hydrated ? "Date to follow" : " "}</span>            )}          </p>          {(timeWindow || address) && !cancelled && (            <p className="mt-1.5 truncate text-[12.5px] text-primary-foreground/70">              {[status === "delivered" ? null : timeWindow, address ? `to ${address}` : null].filter(Boolean).join(" · ")}            </p>          )}          {/* The route: stops on a hairline, the parcel riding to where the order is. */}          <div className="relative mt-5 px-3" style={{ opacity: cancelled ? 0.5 : 1, transition: "opacity 400ms" }}>            <div className="relative h-6">              <span aria-hidden="true" className="absolute inset-x-0 top-1/2 h-px -translate-y-1/2" style={{ background: mix("var(--primary-foreground)", 26) }} />              <span                aria-hidden="true"                className="absolute inset-x-0 top-1/2 h-[2px] origin-left -translate-y-1/2 rounded-full"                style={{ background: "var(--primary-foreground)", transform: `scaleX(${progress})`, transition: reduced ? "none" : `transform ${RIDE}ms ${EASE}` }}              />              <ol className="absolute inset-0 flex items-center justify-between">                {STAGES.map((s, i) => {                  const lit = !cancelled && i <= at;                  return (                    <li key={s.id} className="relative flex size-0 items-center justify-center" aria-current={i === at && !cancelled ? "step" : undefined}>                      <span className="sr-only">{`${s.label}${lit ? (i === at ? ", current" : ", done") : ""}`}</span>                      <span                        aria-hidden="true"                        className="absolute size-2.5 rounded-full"                        style={{                          background: lit ? "var(--primary-foreground)" : "var(--primary)",                          boxShadow: `0 0 0 1.5px ${lit ? "var(--primary-foreground)" : mix("var(--primary-foreground)", 45)}`,                          transform: lit && !reduced ? "scale(1)" : "scale(0.8)",                          transition: reduced ? "none" : `background-color 240ms ${EASE} ${passDelay(i)}ms, transform 420ms ${EASE_THROW} ${passDelay(i)}ms, box-shadow 240ms ${passDelay(i)}ms`,                        }}                      />                    </li>                  );                })}              </ol>              {/* The parcel rides the right edge of a full-width layer that slides in from the left: one composited transform,                    and nothing past the card's right edge to scroll to. */}              <span aria-hidden="true" className="pointer-events-none absolute inset-0" style={{ transform: `translateX(${(progress - 1) * 100}%)`, transition: reduced ? "none" : `transform ${RIDE}ms ${EASE}` }}>                <span                  className="absolute right-0 top-1/2 flex size-7 translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-[9px]"                  style={{ background: "var(--primary-foreground)", color: "var(--primary)", boxShadow: `0 0 0 3px ${mix("var(--primary-foreground)", 22)}`, opacity: cancelled ? 0 : 1, transition: "opacity 300ms" }}                >                  <Package className="size-4" strokeWidth={1.9} />                </span>              </span>            </div>            <div aria-hidden="true" className="relative mt-2 h-4 text-[11px]">              {STAGES.map((s, i) => (                <span                  key={s.id}                  className="absolute top-0 whitespace-nowrap"                  style={{                    left: `${(i / (STAGES.length - 1)) * 100}%`,                    transform: i === 0 ? "translateX(-12px)" : i === STAGES.length - 1 ? "translateX(calc(-100% + 12px))" : "translateX(-50%)",                    color: !cancelled && i <= at ? "var(--primary-foreground)" : mix("var(--primary-foreground)", 55),                    fontWeight: i === at && !cancelled ? 500 : 400,                    transition: `color 240ms ${passDelay(i)}ms`,                  }}                >                  {s.short}                </span>              ))}            </div>          </div>        </div>        {/* Flat: tracking, the scans, and Cancel while it can be. */}        {(carrier || tracking) && (          <div className="mx-1 mt-2 flex h-10 items-center gap-2 rounded-full px-3 text-[13px] shadow-[inset_0_0_0_1px_var(--border)]">            <span className="shrink-0 text-muted-foreground">{carrier ?? "Tracking"}</span>            {tracking && <span className="min-w-0 flex-1 truncate font-mono text-[12.5px] text-foreground">{tracking}</span>}            {tracking && <CopyButton text={tracking} label="Copy tracking number" size="sm" variant="ghost" className="shrink-0" />}            {trackingUrl && (              <a href={trackingUrl} target="_blank" rel="noreferrer" className={`-mr-1 inline-flex h-7 shrink-0 items-center gap-0.5 rounded-full px-2 text-[12.5px] text-primary hover:bg-primary/10 ${INTERACTION_FOCUS}`}>                Track                <ArrowUpRight aria-hidden="true" className="size-3.5" />              </a>            )}          </div>        )}        {events.length > 0 && (          <div className="px-3 pb-1 pt-3">            <div className="flex items-center justify-between">              <h3 className="text-[12.5px] font-normal text-muted-foreground">Updates</h3>              {events.length > visibleEvents && (                <button type="button" aria-expanded={expanded} onClick={() => setExpanded((v) => !v)} className={`-mr-2 cursor-pointer rounded-full px-2 py-1 text-[12.5px] text-foreground hover:bg-accent ${INTERACTION_FOCUS}`}>                  <TextMorph>{expanded ? "Show less" : `Show all ${events.length}`}</TextMorph>                </button>              )}            </div>            <div className="relative mt-1">              <div ref={ghostRef} aria-hidden="true" className="pointer-events-none absolute inset-0" />              <ol ref={listRef} className="relative">                {rows.map((e, i) => {                  const when = new Date(e.at);                  return (                    <li key={e.id} data-row-id={e.id} className="relative flex gap-3 py-2 pl-4">                      <span aria-hidden="true" className={`absolute left-0 top-[13px] size-[7px] rounded-full ${i === 0 && !cancelled ? "bg-primary shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_18%,transparent)]" : "bg-border"}`} />                      {i < rows.length - 1 && <span aria-hidden="true" className="absolute bottom-[-7px] left-[3px] top-[24px] w-px bg-border" />}                      <span className="min-w-0 flex-1">                        <span className={`block text-[13.5px] ${i === 0 ? "font-medium text-foreground" : "text-foreground/85"}`}>{e.title}</span>                        {e.place && <span className="block truncate text-[12px] text-muted-foreground">{e.place}</span>}                      </span>                      <span className="shrink-0 pt-px text-right text-[12px] tabular-nums text-muted-foreground" suppressHydrationWarning>                        {hydrated ? fmt.day.format(when) : ""}                        <span className="block">{hydrated ? fmt.time.format(when) : ""}</span>                      </span>                    </li>                  );                })}              </ol>            </div>          </div>        )}        {canCancel && (          <div className="flex flex-col items-center px-3 pb-1.5 pt-1">            <StatusButton              variant="ghost"              status={cancel.status}              labels={{ idle: cancel.confirm ? "Yes, cancel this order" : "Cancel order", pending: "Cancelling", success: "Cancelled", error: "Try again" }}              onClick={() => void cancelOrder()}              className={`h-9 px-4 text-[13px] ${cancel.confirm && cancel.status === "idle" ? "text-red-600 hover:text-red-600 dark:text-red-400 dark:hover:text-red-400" : ""}`}            />            {cancel.error && (              <p role="alert" className="mt-1 text-center text-[12px] text-red-600 dark:text-red-400">                {cancel.error}              </p>            )}          </div>        )}      </div>    </section>  );}export default OrderStatus;
components/ui-interaction-motion.ts
'use client';import * as React from 'react';export const EASE_OUT = 'cubic-bezier(0.16, 1, 0.3, 1)';export const EASE_SETTLE = 'cubic-bezier(0.22, 1.18, 0.36, 1)';/** Thrown things (indicators, thumbs): a slight overshoot on transform only. */export const EASE_THROW = 'cubic-bezier(0.34, 1.36, 0.64, 1)';/** * The press every button primitive shares: a 3% give that eases in and settles back. * Tailwind v4's scale-* writes the `scale` property, so `scale` has to be in the list * or the press snaps. */export const INTERACTION_PRESS =  'transition-[background-color,color,box-shadow,opacity,scale] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100';export const INTERACTION_FOCUS ='focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background';const query = '(prefers-reduced-motion: reduce)';const subscribe = (notify: () => void) => {  const media = window.matchMedia(query);  media.addEventListener('change', notify);  return () => media.removeEventListener('change', notify);};export const useInteractionReducedMotion = () => React.useSyncExternalStore(subscribe, () => window.matchMedia(query).matches, () => false);export function useInteractionActivity<T extends Element>() {  const ref = React.useRef<T>(null);  const [visible, setVisible] = React.useState(false);  const [hidden, setHidden] = React.useState(false);  React.useEffect(() => {    const el = ref.current;    if (!el) return;    const observer = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting));    observer.observe(el);    const visibility = () => setHidden(document.hidden);    visibility(); document.addEventListener('visibilitychange', visibility);    return () => { observer.disconnect(); document.removeEventListener('visibilitychange', visibility); };  }, []);  return { ref, active: visible && !hidden };}export function interactionIcon(on: boolean, reduced: boolean): React.CSSProperties {  return { opacity: on ? 1 : 0, transform: on ? 'none' : 'scale(0.6)', filter: on ? 'blur(0px)' : 'blur(3px)', transition: reduced ? 'none' : `opacity 220ms ${EASE_OUT}, transform 380ms ${EASE_OUT}, filter 220ms ${EASE_OUT}` };}/** Sample recent movement rather than averaging a long drag and losing its release velocity. */export type PointerSample = { x: number; time: number };export function samplePointer(samples: PointerSample[], x: number, time: number) {  samples.push({ x, time });  while (samples.length > 2 && time - samples[0].time > 100) samples.shift();}export function releaseVelocity(samples: PointerSample[], now: number) {  if (samples.length < 2 || now - samples[samples.length - 1].time > 100) return 0;  const first = samples[0], last = samples[samples.length - 1];  return (last.x - first.x) / Math.max(1, last.time - first.time);}/** Smooth Apple/rational rubber-band curve: continuous exponential-like resistance without linear kinks. */export function rubberBand(overshoot: number, dimension = 320, constant = 0.55): number {  const d = dimension || 320;  const abs = Math.abs(overshoot);  return Math.sign(overshoot) * (1 - 1 / ((abs * constant) / d + 1)) * d;}/** Projects resting travel from release velocity (velocity in px/ms; time in seconds). */export function projectInertia(distance: number, velocity: number, time = 0.12): number {  return distance + velocity * 1000 * time;}/** Smooths incoming release velocity toward a boundary so springs don't slam or clip into walls. */export function softenVelocity(velocity: number, distanceToTarget: number, factor = 10): number {  const maxAllowed = Math.abs(distanceToTarget) * factor;  return Math.sign(velocity) * Math.min(Math.abs(velocity), maxAllowed);}/** A damped spring sampled by WAAPI: tuned for organic lightness with a hint of life (3% breath). */export function springFrames(  from: number,  to: number,  velocity = 0,  duration = 480,  config: { omega?: number; damping?: number; steps?: number } = {},) {  const { omega = 16, damping = 0.74, steps = 36 } = config;  const damped = omega * Math.sqrt(Math.max(0.001, 1 - damping * damping));  const displacement = from - to;  const coefficient = (velocity * 1000 + damping * omega * displacement) / damped;  return Array.from({ length: steps + 1 }, (_, i) => {    const t = (i / steps) * (duration / 1000);    const value = to + Math.exp(-damping * omega * t) * (displacement * Math.cos(damped * t) + coefficient * Math.sin(damped * t));    return { value: i === steps ? to : value, offset: i / steps };  });}
components/use-row-motion.ts
"use client";import * as React from "react";/* ───────────────────────────────────────────────────────── * ROW MOTION: rows that keep their place on screen * *   glide     after any render (a sort, a filter, new data from *             the server) every row that moved glides from its *             old place to its new one. Glides are additive, so *             a change that lands mid-glide adds its own offset *             instead of restarting anything *   travel    a row that sorts onto the page slides in from the *             side where it ranked before; one that sorts off *             slides out toward its new rank *   enter     a row that's new to the list fades in where it *             lands while the rows below glide clear of it *   leave     a row that's gone folds away where it stood. Its *             copy is the row's own element, detached by React *             and moved into a layer React never renders into, *             so a leaving row costs no render at all *   stagger   a sort can ask for the rows to set off one after *             another, top first * * Positions are layout offsets (transforms ignored), kept after * every render and whenever the list changes size (a row * opening its detail), so they're never stale. Rows are found * by data-row-id; ranks are their places in the whole sorted * list, not just the page. * ───────────────────────────────────────────────────────── */type Seen = { top: number; height: number; el: HTMLElement };const EASE = "cubic-bezier(0.16,1,0.3,1)";const GLIDE = 480;export function useRowMotion(  container: React.RefObject<HTMLElement | null>,  { reduced, resetKey, ranks, ghosts }: { reduced: boolean; resetKey?: string | number; ranks: ReadonlyMap<string, number>; ghosts: React.RefObject<HTMLElement | null> },) {  const seen = React.useRef(new Map<string, Seen>());  const lastRanks = React.useRef(ranks);  const lastKey = React.useRef(resetKey);  const staggerNext = React.useRef(false);  const read = React.useCallback(() => {    const out = new Map<string, Seen>();    container.current?.querySelectorAll<HTMLElement>("[data-row-id]").forEach((el) => out.set(el.dataset.rowId!, { top: el.offsetTop, height: el.offsetHeight, el }));    return out;  }, [container]);  /** The row React just removed, back on screen where it stood, leaving: sliding toward where it went, or folding. */  const leave = (was: Seen, dy: number) => {    const layer = ghosts.current;    const el = was.el;    if (!layer || el.isConnected) return;    // No getAnimations() here (it flushes style for the whole page): the exit below is added last and replaces any    // glide still on the element.    el.removeAttribute("data-row-id");    el.removeAttribute("tabindex");    el.setAttribute("aria-hidden", "true");    el.inert = true;    Object.assign(el.style, { position: "absolute", left: "0", right: "0", top: `${was.top}px`, height: `${was.height}px`, margin: "0", overflow: "hidden" });    layer.appendChild(el);    const a = el.animate(      dy        ? [            { transform: "translateY(0px)", opacity: 1, filter: "blur(0px)" },            { transform: `translateY(${dy}px)`, opacity: 0, filter: "blur(3px)" },          ]        : [            { opacity: 1, transform: "none", filter: "blur(0px)" },            { opacity: 0, transform: "scale(0.985)", filter: "blur(4px)" },          ],      { duration: dy ? 360 : 280, easing: EASE, fill: "forwards" },    );    a.onfinish = a.oncancel = () => el.remove();  };  React.useLayoutEffect(() => {    const el = container.current;    if (!el) return;    const before = seen.current;    const after = read();    const prevRanks = lastRanks.current;    seen.current = after;    lastRanks.current = ranks;    const stagger = staggerNext.current;    staggerNext.current = false;    if (lastKey.current !== resetKey) {      // A different page: the caller slides the whole body; nothing glides and nothing folds.      lastKey.current = resetKey;      return;    }    if (reduced || !before.size) return;    // A typical row's height. Rows that come onto or leave the page only hint at the way they went (a row and a    // quarter at most), so the list never shows holes while they travel.    const heights = [...after.values()].map((b) => b.height).sort((a, b) => a - b);    const rowH = heights[Math.floor(heights.length / 2)] ?? 48;    const clamp = (dy: number) => Math.max(-rowH * 1.25, Math.min(rowH * 1.25, dy));    let order = 0;    after.forEach((now, id) => {      const row = now.el;      const was = before.get(id);      const delay = stagger ? Math.min(order++ * 16, 200) : 0;      if (was) {        const dy = was.top - now.top;        if (Math.abs(dy) < 0.5) return;        row.animate([{ transform: `translateY(${dy}px)`, zIndex: 1 }, { transform: "translateY(0px)", zIndex: 1 }], { duration: GLIDE, delay, easing: EASE, composite: "add", fill: "backwards" });        return;      }      const from = prevRanks.get(id);      const to = ranks.get(id);      if (from !== undefined && to !== undefined) {        // It ranked somewhere off the page: it comes in from that side.        row.animate(          [            { transform: `translateY(${clamp((from - to) * rowH)}px)`, opacity: 0, filter: "blur(3px)" },            { transform: "translateY(0px)", opacity: 1, filter: "blur(0px)" },          ],          { duration: GLIDE, delay: delay / 2, easing: EASE, fill: "backwards" },        );        return;      }      row.animate(        [          { opacity: 0, filter: "blur(4px)" },          { opacity: 1, filter: "blur(0px)" },        ],        { duration: 420, delay: 90, easing: EASE, fill: "backwards" },      );    });    // Still in the list, just not on this page: its copy slides toward where it went. Gone for good: it folds.    before.forEach((was, id) => {      if (after.has(id)) return;      const from = prevRanks.get(id);      const to = ranks.get(id);      leave(was, from !== undefined && to !== undefined ? clamp((to - from) * rowH) || rowH : 0);    });  });  // The list changed size without a render of ours (a detail opening by transition): keep the positions true.  React.useEffect(() => {    const el = container.current;    if (!el) return;    const observer = new ResizeObserver(() => {      seen.current = read();    });    observer.observe(el);    return () => observer.disconnect();  }, [container, read]);  return {    /** The next change (a sort) sets the rows off one after another. */    stagger: () => {      staggerNext.current = true;    },  };}

Registry item: /r/order-status.json