Defense & Aerospace

Blue Force Tracker

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.

Install

npx shadcn@latest add @paragon/blue-force-tracker

Also installs: world-map

blue-force-tracker.tsx

"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>
  );
}