{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "maritime-vessel-map",
  "title": "Maritime Vessel Map",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/maritime-vessel-map/maritime-vessel-map.component.ts",
      "content": "// Automatic Identification System (AIS) vessel tracking with container ships, tankers,\n// speed, and anchorage status on a Mapbox map. Selecting a vessel (pin or list row) flies\n// the camera there. Port of the React block 1:1; markers follow the asset-tracking-map\n// Angular pattern (same map lib as Vue/React: mapbox-gl via UiMapComponent).\nimport {\n  type AfterViewInit,\n  ChangeDetectionStrategy,\n  Component,\n  type ElementRef,\n  Input,\n  type OnDestroy,\n  type OnInit,\n  type QueryList,\n  ViewChildren,\n  signal,\n} from '@angular/core'\nimport type * as mapboxgl from 'mapbox-gl'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardDescriptionComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiMapComponent } from '@/ui/map/map.component'\n\nexport type VesselType = 'Container' | 'Crude Tanker' | 'Bulk Carrier' | 'Tug'\nexport type VesselStatus = 'Underway' | 'Moored' | 'At Anchor'\n\nexport interface Vessel {\n  mmsi: string\n  name: string\n  flag: string\n  type: VesselType\n  coords: [number, number]\n  heading: number\n  speedKnots: number\n  draught: string\n  dest: string\n  eta: string\n  status: VesselStatus\n}\n\nexport const DEFAULT_VESSELS: Vessel[] = [\n  {\n    mmsi: '219018442',\n    name: 'EVER GIVEN',\n    flag: 'PA',\n    type: 'Container',\n    coords: [103.78, 1.25],\n    heading: 110,\n    speedKnots: 14.8,\n    draught: '15.7m',\n    dest: 'SGSIN PEB',\n    eta: 'OCT 24 14:00',\n    status: 'Underway',\n  },\n  {\n    mmsi: '636019812',\n    name: 'NORDIC STAR',\n    flag: 'LR',\n    type: 'Crude Tanker',\n    coords: [103.88, 1.22],\n    heading: 245,\n    speedKnots: 0.2,\n    draught: '12.1m',\n    dest: 'SINGAPORE ANCH',\n    eta: 'OCT 24 08:30',\n    status: 'At Anchor',\n  },\n  {\n    mmsi: '356782000',\n    name: 'PACIFIC RUBY',\n    flag: 'SG',\n    type: 'Bulk Carrier',\n    coords: [103.95, 1.29],\n    heading: 45,\n    speedKnots: 11.2,\n    draught: '10.4m',\n    dest: 'JPTYO',\n    eta: 'OCT 29 06:00',\n    status: 'Underway',\n  },\n  {\n    mmsi: '508111222',\n    name: 'HARBOR VALIANT',\n    flag: 'SG',\n    type: 'Tug',\n    coords: [103.75, 1.28],\n    heading: 180,\n    speedKnots: 6.4,\n    draught: '4.5m',\n    dest: 'PASIR PANJANG',\n    eta: 'OCT 24 12:00',\n    status: 'Underway',\n  },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-maritime-vessel-map, [ui-maritime-vessel-map]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiMapComponent,\n  ],\n  template: `\n    <div data-slot=\"maritime-vessel-map\" [class]=\"rootClass\">\n      <div class=\"border-border bg-card relative h-[480px] overflow-hidden rounded-xl border\">\n        <div\n          ui-map\n          [accessToken]=\"accessToken ?? ''\"\n          variant=\"dark\"\n          [center]=\"activeVessel().coords\"\n          [zoom]=\"10.8\"\n          class=\"h-full w-full\"\n          (created)=\"onMapCreated($event)\"\n        >\n          <!-- Marker elements: handed to mapbox-gl Markers once the map exists, which moves the\n               inner #marker node onto the map; the outer node stays put for Angular's @for. -->\n          <div class=\"hidden\">\n            @for (v of vessels; track v.mmsi) {\n              <div>\n                <div #marker>\n                  <button\n                    type=\"button\"\n                    class=\"group relative flex cursor-pointer flex-col items-center\"\n                    (click)=\"selectVessel(v)\"\n                  >\n                    <div [class]=\"pinClass(v.mmsi)\" [style.transform]=\"'rotate(' + v.heading + 'deg)'\">▲</div>\n                    <span [class]=\"labelClass(v.mmsi)\">{{ v.name }}</span>\n                  </button>\n                </div>\n              </div>\n            }\n          </div>\n        </div>\n      </div>\n\n      <!-- AIS Telemetry Drawer -->\n      <div ui-card class=\"flex flex-col justify-between\">\n        <div ui-card-header>\n          <div class=\"flex items-center justify-between\">\n            <span ui-badge variant=\"outline\" class=\"font-mono text-xs\">{{ activeVessel().type }}</span>\n            <span ui-badge class=\"border-cyan-500/20 bg-cyan-500/10 text-cyan-600 dark:text-cyan-400\">{{\n              activeVessel().status\n            }}</span>\n          </div>\n          <h3 ui-card-title class=\"mt-2 font-mono text-lg\">{{ activeVessel().name }}</h3>\n          <p ui-card-description class=\"font-mono text-xs\">\n            MMSI: {{ activeVessel().mmsi }} • Flag: {{ activeVessel().flag }}\n          </p>\n        </div>\n        <div ui-card-content class=\"space-y-4\">\n          <div class=\"border-border bg-muted/40 grid grid-cols-2 gap-2 rounded-lg border p-2.5 font-mono text-xs\">\n            <div>\n              <div class=\"text-muted-foreground text-xs uppercase\">Speed Over Ground</div>\n              <div class=\"text-foreground text-sm font-bold\">{{ activeVessel().speedKnots }} kts</div>\n            </div>\n            <div>\n              <div class=\"text-muted-foreground text-xs uppercase\">Heading</div>\n              <div class=\"text-foreground text-sm font-bold\">{{ activeVessel().heading }}°</div>\n            </div>\n            <div class=\"mt-2\">\n              <div class=\"text-muted-foreground text-xs uppercase\">Max Draught</div>\n              <div class=\"text-foreground text-sm font-bold\">{{ activeVessel().draught }}</div>\n            </div>\n            <div class=\"mt-2\">\n              <div class=\"text-muted-foreground text-xs uppercase\">Destination</div>\n              <div class=\"text-foreground truncate text-xs font-bold\">{{ activeVessel().dest }}</div>\n            </div>\n          </div>\n\n          <div class=\"border-border space-y-1 border-t pt-3\">\n            <div class=\"text-muted-foreground mb-1 font-mono text-xs tracking-wider uppercase\">\n              Singapore Strait Traffic\n            </div>\n            @for (v of vessels; track v.mmsi) {\n              <button type=\"button\" [class]=\"rowClass(v.mmsi)\" (click)=\"selectVessel(v)\">\n                <span class=\"font-mono\">{{ v.name }}</span>\n                <span class=\"font-mono text-xs opacity-75\">{{ v.speedKnots }} kts</span>\n              </button>\n            }\n          </div>\n\n          <button ui-button variant=\"outline\" size=\"sm\" class=\"w-full font-mono text-xs\" (click)=\"recenter()\">\n            Center on Vessel\n          </button>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiMaritimeVesselMapComponent implements OnInit, AfterViewInit, OnDestroy {\n  @Input() vessels: Vessel[] = DEFAULT_VESSELS.map((v) => ({ ...v, coords: [...v.coords] as [number, number] }))\n  @Input() accessToken?: string\n  @Input('class') className?: string\n\n  @ViewChildren('marker') private markerEls?: QueryList<ElementRef<HTMLDivElement>>\n\n  readonly activeMmsi = signal('')\n\n  private map: mapboxgl.Map | null = null\n  private markers: mapboxgl.Marker[] = []\n  private destroyed = false\n\n  ngOnInit(): void {\n    this.activeMmsi.set(this.vessels[0]?.mmsi ?? '')\n  }\n\n  ngAfterViewInit(): void {\n    // A new vessels list re-renders the marker elements; put the new set on the map.\n    this.markerEls?.changes.subscribe(() => void this.syncMarkers())\n  }\n\n  ngOnDestroy(): void {\n    this.destroyed = true\n    this.clearMarkers()\n    this.map = null\n  }\n\n  get rootClass(): string {\n    return cn('grid gap-4 lg:grid-cols-[1fr_320px]', this.className)\n  }\n\n  activeVessel(): Vessel {\n    return this.vessels.find((v) => v.mmsi === this.activeMmsi()) ?? this.vessels[0]!\n  }\n\n  pinClass(mmsi: string): string {\n    return cn(\n      'flex size-7 items-center justify-center rounded-full font-bold text-cyan-400',\n      this.activeMmsi() === mmsi ? 'bg-cyan-500/20 ring-2 ring-cyan-400' : 'bg-background/80',\n    )\n  }\n\n  labelClass(mmsi: string): string {\n    return cn(\n      'border-border bg-card/90 mt-1 rounded border px-1 py-0.5 font-mono text-xs font-semibold tracking-wider whitespace-nowrap shadow-xs',\n      this.activeMmsi() === mmsi ? 'border-cyan-400/50 text-cyan-400' : 'text-foreground',\n    )\n  }\n\n  rowClass(mmsi: string): string {\n    return cn(\n      'flex w-full items-center justify-between rounded-md px-2 py-1 text-xs transition-colors',\n      this.activeMmsi() === mmsi\n        ? 'bg-accent text-accent-foreground font-semibold'\n        : 'hover:bg-muted text-muted-foreground',\n    )\n  }\n\n  onMapCreated(map: mapboxgl.Map): void {\n    this.map = map\n    void this.syncMarkers()\n  }\n\n  selectVessel(v: Vessel): void {\n    this.activeMmsi.set(v.mmsi)\n    this.map?.flyTo?.({ center: v.coords, zoom: 11.5 })\n  }\n\n  recenter(): void {\n    const active = this.activeVessel()\n    if (active) this.selectVessel(active)\n  }\n\n  /** One mapbox-gl Marker per vessel, anchored at its centre (the React MapMarker, anchor=\"center\"). */\n  private async syncMarkers(): Promise<void> {\n    const map = this.map\n    const els = this.markerEls?.toArray() ?? []\n    if (!map) return\n    const mod = await import('mapbox-gl')\n    const mapbox = mod.default ?? mod\n    if (this.destroyed || map !== this.map) return\n    this.clearMarkers()\n    this.markers = els.flatMap((el, i) => {\n      const vessel = this.vessels[i]\n      if (!vessel) return []\n      return [new mapbox.Marker({ element: el.nativeElement, anchor: 'center' }).setLngLat(vessel.coords).addTo(map)]\n    })\n  }\n\n  private clearMarkers(): void {\n    for (const m of this.markers) m.remove()\n    this.markers = []\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/maritime-vessel-map/maritime-vessel-map.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/maritime-vessel-map/index.ts",
      "content": "export {\n  UiMaritimeVesselMapComponent,\n  DEFAULT_VESSELS,\n  type Vessel,\n  type VesselType,\n  type VesselStatus,\n} from './maritime-vessel-map.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/maritime-vessel-map/index.ts"
    }
  ],
  "dependencies": [
    "mapbox-gl",
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/map.json"
  ],
  "description": "Automatic Identification System (AIS) vessel tracking with container ships, tankers, speed, and anchorage status.",
  "categories": [
    "logistics",
    "maritime",
    "map"
  ]
}