Friendly units geo-projected onto accurate world geometry with domain-typed glyphs, a freshness ring encoding time since last report, and a roster that pings unit locations.
npx shadcn@latest add @paragon/blue-force-trackerAlso installs: world-map
"use client";
import * as React from "react";
import { Plane, Ship, Truck, Radio, User } from "lucide-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";
const [, , VB_W, VB_H] = WORLD_VIEWBOX.split(" ").map(Number);
export type UnitDomain = "air" | "sea" | "ground" | "sof" | "c2";
export interface BlueUnit {
id: string;
callsign: string;
domain: UnitDomain;
lng: number;
lat: number;
/** Seconds since last position report. */
lastSeen: number;
}
export interface BlueForceTrackerProps extends React.ComponentProps<"div"> {
units?: BlueUnit[];
}
const DOMAIN_ICON: Record<UnitDomain, React.ElementType> = {
air: Plane,
sea: Ship,
ground: Truck,
sof: User,
c2: Radio,
};
const DEFAULT_UNITS: BlueUnit[] = [
{ id: "u1", callsign: "REAPER 1-1", domain: "air", lng: 44, lat: 33, lastSeen: 4 },
{ id: "u2", callsign: "CSG PARAGON", domain: "sea", lng: 57, lat: 22, lastSeen: 12 },
{ id: "u3", callsign: "DAGGER 6", domain: "ground", lng: 45, lat: 31, lastSeen: 61 },
{ id: "u4", callsign: "GHOST 3", domain: "sof", lng: 41, lat: 36, lastSeen: 9 },
{ id: "u5", callsign: "SENTRY", domain: "c2", lng: 50, lat: 27, lastSeen: 2 },
];
function seenClass(s: number) {
if (s <= 15) return "var(--color-success)";
if (s <= 60) return "var(--color-warning)";
return "var(--color-destructive)";
}
function seenLabel(s: number) {
if (s < 60) return `${s}s ago`;
const m = Math.floor(s / 60);
return `${m}m ago`;
}
/**
* A blue-force situational display. Friendly units are geo-projected onto the
* accurate `world-map` and rendered as domain-typed glyphs (air / sea / ground
* / SOF / C2); a color-coded freshness ring encodes time since last position
* report. Selecting a unit in the roster pings its location. Purely
* transform/opacity motion, reduced-motion safe via the shared ping.
*/
export function BlueForceTracker({
units = DEFAULT_UNITS,
className,
...props
}: BlueForceTrackerProps) {
const [sel, setSel] = React.useState<string | null>(null);
const points = React.useMemo(
() =>
units.map((u) => {
const p = project(u.lng, u.lat);
return { ...u, x: p.x, y: p.y };
}),
[units],
);
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_220px]",
className,
)}
{...props}
>
<div className="relative overflow-hidden rounded-lg">
<WorldMap tooltip={false} className="[&_svg]:opacity-90" />
<svg
viewBox={WORLD_VIEWBOX}
className="pointer-events-none absolute inset-0 h-full w-full"
>
{points.map((p) => {
const active = p.id === sel;
return (
<g key={p.id}>
{active && (
<circle
cx={p.x}
cy={p.y}
r={3}
fill="var(--color-primary)"
className="pg-map-ping"
/>
)}
<circle
cx={p.x}
cy={p.y}
r={active ? 4 : 3.2}
fill="var(--color-card)"
stroke={seenClass(p.lastSeen)}
strokeWidth={1.4}
vectorEffect="non-scaling-stroke"
style={{ transition: "r 150ms var(--ease-out)" }}
/>
<circle cx={p.x} cy={p.y} r={1.3} fill="var(--color-primary)" />
</g>
);
})}
</svg>
</div>
<ul className="flex flex-col gap-1 overflow-y-auto p-1" style={{ maxHeight: 260 }}>
<li className="px-1 pb-1 text-[10px] uppercase tracking-wider text-muted-foreground">
Blue Force · {units.length}
</li>
{units.map((u) => {
const Icon = DOMAIN_ICON[u.domain];
const active = u.id === sel;
return (
<li key={u.id}>
<button
type="button"
onMouseEnter={() => setSel(u.id)}
onFocus={() => setSel(u.id)}
onClick={() => setSel(active ? null : u.id)}
className={cn(
"pressable flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left outline-none transition-colors duration-150",
active ? "bg-secondary" : "hover:bg-secondary/60 focus-visible:bg-secondary/60",
)}
>
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 truncate font-mono text-xs font-medium">
{u.callsign}
</span>
<span
className="shrink-0 tabular-nums text-[10px]"
style={{ color: seenClass(u.lastSeen) }}
>
{seenLabel(u.lastSeen)}
</span>
</button>
</li>
);
})}
</ul>
</div>
);
}