Flight itinerary timeline with local times as titles, airport codes in Geist Mono tags, a layover row between legs, and a plane mark with a Boarding badge on the current leg.
import { AirplaneTiltIcon } from "@phosphor-icons/react/dist/ssr"
import {
Timeline,
TimelineContent,
TimelineDescription,
TimelineIndicator,
TimelineItem,
TimelineLabel,
TimelineTitle,
} from "@/components/editorial/timeline"
import { Badge } from "@/components/ui/badge"
// The copy: change text here, not in the markup.
const COPY = {
title: "Lisbon to Tokyo",
body: "Thursday 30 October · 1 stop · 15h 40m · local times",
boarding: "Boarding",
layover: "Layover",
} as const
const LEGS = [
{
time: "09:35",
code: "LIS",
place: "Lisbon Humberto Delgado",
detail: "Terminal 1 · Gate 14",
current: true,
},
{
time: "13:50",
code: "FRA",
place: "Frankfurt",
detail: "Arrives · Terminal 1",
layover: "2h 10m",
},
{
time: "16:00",
code: "FRA",
place: "Frankfurt",
detail: "Terminal 1 · Gate Z23",
},
{
time: "10:15 +1",
code: "HND",
place: "Tokyo Haneda",
detail: "Arrives Friday · Terminal 3",
},
] as const
// A trip is one subject in time order: the time is the title, the airport
// code a mono tag, the layover a muted row between legs. The plane marks the
// leg you are on.
export default function FlightItinerary() {
return (
<section className="mx-auto w-full max-w-md">
<header className="mb-6">
<h2 className="text-lg font-semibold tracking-tight">{COPY.title}</h2>
<p className="mt-1 text-sm text-muted-foreground">{COPY.body}</p>
</header>
<Timeline className="[--timeline-lh:1.5rem]">
{LEGS.map((l) => {
const current = "current" in l
return (
<TimelineItem key={l.time + l.code}>
{current ? (
<TimelineIndicator>
<span className="flex size-4 items-center justify-center rounded-full bg-foreground text-background">
<AirplaneTiltIcon
className="size-2.5"
weight="fill"
aria-hidden
/>
</span>
</TimelineIndicator>
) : (
<TimelineIndicator className="border-border [&_[data-slot=timeline-dot]]:bg-muted-foreground/60" />
)}
<TimelineContent className="pb-7">
<div className="flex items-center gap-3">
<TimelineTitle className="text-base leading-6 tabular-nums">
{l.time}
</TimelineTitle>
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
{l.code}
</code>
{current && <Badge>{COPY.boarding}</Badge>}
</div>
<TimelineDescription className="mt-0.5 text-foreground">
{l.place}
</TimelineDescription>
<TimelineLabel className="mt-0.5">{l.detail}</TimelineLabel>
{"layover" in l && (
<TimelineLabel className="mt-3 rounded-md bg-muted px-2 py-1">
{COPY.layover} · {l.layover}
</TimelineLabel>
)}
</TimelineContent>
</TimelineItem>
)
})}
</Timeline>
</section>
)
}
pnpm dlx shadcn add @01orbital/flight-itinerarynpx shadcn add @01orbital/flight-itineraryyarn shadcn add @01orbital/flight-itinerarybunx --bun shadcn add @01orbital/flight-itineraryFree under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/blocks/timeline/flight-itinerary/index.tsx, as source you own and edit.
import FlightItinerary from "@/components/blocks/timeline/flight-itinerary"
export default function Example() {
return <FlightItinerary />
}npm: @phosphor-icons/react. The CLI installs them for you.
Registry pieces it installs with it:
badgefont-geist-monoCourse Progress
Course outline with a progress bar above a numbered lesson timeline: checks for watched lessons, a play mark and Resume link on the current one, and durations on the right.
Incident Timeline
Status-page incident report with a Resolved badge, duration and affected services in the header, then four updates newest first, from Resolved back to Investigating, each with a time and note.