An ISR track table with classification tone, bearing, speed, and confidence bars, wired to a mini world-map so hovering a row pings the track's geo-projected location.
npx shadcn@latest add @paragon/asset-track-tableAlso installs: world-map
"use client";
import * as React from "react";
import { WorldMap, project } from "@/registry/paragon/ui/world-map";
import { WORLD_VIEWBOX } from "@/registry/paragon/ui/world-map-data";
import { cn } from "@/lib/utils";
export type TrackClass = "hostile" | "unknown" | "friendly" | "neutral";
export interface AssetTrack {
id: string;
classification: TrackClass;
type: string;
lng: number;
lat: number;
/** Bearing in degrees. */
bearing: number;
/** Speed in knots. */
speed: number;
/** Track confidence, 0–1. */
confidence: number;
}
export interface AssetTrackTableProps extends React.ComponentProps<"div"> {
tracks?: AssetTrack[];
}
const CLASS_TONE: Record<TrackClass, string> = {
hostile: "var(--color-destructive)",
unknown: "var(--color-warning)",
friendly: "var(--color-success)",
neutral: "var(--color-muted-foreground)",
};
const DEFAULT_TRACKS: AssetTrack[] = [
{ id: "TK-1174", classification: "hostile", type: "Fast air", lng: 51, lat: 26, bearing: 210, speed: 420, confidence: 0.94 },
{ id: "TK-2251", classification: "unknown", type: "Surface", lng: 44, lat: 15, bearing: 300, speed: 18, confidence: 0.61 },
{ id: "BF-09", classification: "friendly", type: "CAP", lng: 55, lat: 24, bearing: 45, speed: 480, confidence: 0.99 },
{ id: "TK-2309", classification: "unknown", type: "Slow mover", lng: 60, lat: 20, bearing: 275, speed: 90, confidence: 0.44 },
{ id: "CV-77", classification: "neutral", type: "Merchant", lng: 58, lat: 18, bearing: 95, speed: 14, confidence: 0.72 },
];
/**
* An ISR track table wired to a mini common-operating-picture. Hovering or
* focusing a row geo-projects the track onto the accurate `world-map` and
* fires the shared map ping — table and map share one coordinate space. A
* confidence bar reads as a proportion; classification is color-toned.
* Fully keyboard-navigable rows.
*/
export function AssetTrackTable({
tracks = DEFAULT_TRACKS,
className,
...props
}: AssetTrackTableProps) {
const [active, setActive] = React.useState<string | null>(tracks[0]?.id ?? null);
const point = React.useMemo(() => {
const t = tracks.find((x) => x.id === active);
return t ? { ...project(t.lng, t.lat), tone: CLASS_TONE[t.classification] } : null;
}, [active, tracks]);
return (
<div
className={cn(
"grid w-full max-w-3xl gap-2 rounded-xl bg-card p-2 text-card-foreground shadow-border sm:grid-cols-[1fr_200px]",
className,
)}
{...props}
>
<div className="overflow-hidden rounded-lg border border-border">
<table className="w-full border-collapse text-xs">
<thead>
<tr className="border-b border-border bg-secondary/50 text-[10px] uppercase tracking-wider text-muted-foreground">
<th className="px-2 py-1.5 text-left font-medium">Track</th>
<th className="px-2 py-1.5 text-left font-medium">Type</th>
<th className="px-2 py-1.5 text-right font-medium">Brg</th>
<th className="px-2 py-1.5 text-right font-medium">Spd</th>
<th className="px-2 py-1.5 text-left font-medium">Conf</th>
</tr>
</thead>
<tbody>
{tracks.map((t) => {
const on = t.id === active;
return (
<tr
key={t.id}
tabIndex={0}
onMouseEnter={() => setActive(t.id)}
onFocus={() => setActive(t.id)}
className={cn(
"cursor-default border-b border-border/60 outline-none transition-colors duration-150 last:border-0",
on ? "bg-secondary" : "hover:bg-secondary/50 focus-visible:bg-secondary/50",
)}
>
<td className="px-2 py-1.5">
<span className="inline-flex items-center gap-1.5 font-mono font-medium">
<span
className="size-1.5 shrink-0 rounded-full"
style={{ backgroundColor: CLASS_TONE[t.classification] }}
/>
{t.id}
</span>
</td>
<td className="px-2 py-1.5 text-muted-foreground">{t.type}</td>
<td className="px-2 py-1.5 text-right tabular-nums">{t.bearing}°</td>
<td className="px-2 py-1.5 text-right tabular-nums">{t.speed}</td>
<td className="px-2 py-1.5">
<span className="flex items-center gap-1.5">
<span className="h-1 w-10 overflow-hidden rounded-full bg-secondary">
<span
className="block h-full rounded-full transition-[width] duration-(--duration-base) ease-(--ease-out)"
style={{
width: `${Math.round(t.confidence * 100)}%`,
backgroundColor: CLASS_TONE[t.classification],
}}
/>
</span>
<span className="w-7 shrink-0 text-right tabular-nums text-[10px] text-muted-foreground">
{Math.round(t.confidence * 100)}%
</span>
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="relative overflow-hidden rounded-lg border border-border">
<WorldMap tooltip={false} className="[&_svg]:opacity-90" />
{point && (
<svg
viewBox={WORLD_VIEWBOX}
className="pointer-events-none absolute inset-0 h-full w-full"
>
<circle cx={point.x} cy={point.y} r={3} fill={point.tone} className="pg-map-ping" />
<circle
cx={point.x}
cy={point.y}
r={2.4}
fill={point.tone}
stroke="var(--color-card)"
strokeWidth={0.8}
/>
</svg>
)}
</div>
</div>
);
}