{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "leaflet-maritime-vessel-map",
  "title": "Leaflet Maritime Vessel Map",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/leaflet-maritime-vessel-map/leaflet-maritime-vessel-map.component.ts",
      "content": "// Automatic Identification System (AIS) vessel tracking on free OpenStreetMap/Esri tiles —\n// no API key. Container ships, tankers, shipping lanes, and anchorage status.\n// Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, ViewChild, signal } from '@angular/core'\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 {\n  UiLeafletMapComponent,\n  UiLeafletMarkerComponent,\n  UiLeafletPolylineComponent,\n} from '@/ui/leaflet-map/leaflet-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  /** [lng, lat] — Mapbox order. */\n  coords: [number, number]\n  heading: number\n  speedKnots: number\n  draught: string\n  dest: string\n  eta: string\n  status: VesselStatus\n}\n\n// Module-level constant: a stable reference keeps the map's center input\n// from re-firing setView on every render and cancelling flyTo animations.\nexport const MAP_CENTER: [number, number] = [4.4, 51.9]\n\nexport const DEFAULT_VESSELS: Vessel[] = [\n  {\n    mmsi: '219018442',\n    name: 'EVER GIVEN',\n    flag: 'PA',\n    type: 'Container',\n    coords: [4.04, 51.98],\n    heading: 95,\n    speedKnots: 13.8,\n    draught: '15.7m',\n    dest: 'RTM MAASVLAKTE',\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: [4.18, 51.94],\n    heading: 265,\n    speedKnots: 0.3,\n    draught: '12.1m',\n    dest: 'EUROPOORT 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: [4.33, 51.92],\n    heading: 100,\n    speedKnots: 10.6,\n    draught: '10.4m',\n    dest: 'NLRTM ECT',\n    eta: 'OCT 29 06:00',\n    status: 'Underway',\n  },\n  {\n    mmsi: '508111222',\n    name: 'HARBOR VALIANT',\n    flag: 'NL',\n    type: 'Tug',\n    coords: [4.47, 51.9],\n    heading: 180,\n    speedKnots: 6.4,\n    draught: '4.5m',\n    dest: 'WAALHAVEN',\n    eta: 'OCT 24 12:00',\n    status: 'Underway',\n  },\n]\n\n// Nieuwe Waterweg main channel (North Sea -> Waalhaven) + Maasvlakte approach lane.\nexport const SHIPPING_LANES: [number, number][][] = [\n  [\n    [3.94, 52.0],\n    [4.08, 51.965],\n    [4.22, 51.935],\n    [4.36, 51.915],\n    [4.55, 51.9],\n  ],\n  [\n    [3.99, 52.03],\n    [4.14, 51.99],\n    [4.28, 51.945],\n    [4.45, 51.905],\n  ],\n]\n\nexport interface PortTerminal {\n  name: string\n  coords: [number, number]\n}\n\nexport const PORTS: PortTerminal[] = [\n  { name: 'MAASVLAKTE', coords: [3.98, 51.99] },\n  { name: 'EUROPOORT', coords: [4.09, 51.95] },\n  { name: 'WAALHAVEN', coords: [4.42, 51.885] },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-leaflet-maritime-vessel-map, [ui-leaflet-maritime-vessel-map]',\n  standalone: true,\n  imports: [\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiLeafletMapComponent,\n    UiLeafletMarkerComponent,\n    UiLeafletPolylineComponent,\n  ],\n  host: {\n    'data-slot': 'leaflet-maritime-vessel-map',\n    '[class]': 'hostClass',\n  },\n  template: `\n    <div class=\"border-border bg-card relative h-[480px] overflow-hidden rounded-xl border\">\n      <ui-leaflet-map variant=\"dark\" [center]=\"mapCenter\" [zoom]=\"11\" class=\"h-full w-full\">\n        <!-- Shipping lanes -->\n        @for (lane of shippingLanes; track $index) {\n          <ui-leaflet-polyline\n            [lngLatPath]=\"lane\"\n            color=\"#22d3ee\"\n            [weight]=\"2\"\n            [opacity]=\"0.45\"\n            dashArray=\"6 6\"\n            lineCap=\"round\"\n          />\n        }\n\n        <!-- Port terminals -->\n        @for (p of ports; track p.name) {\n          <ui-leaflet-marker [lngLat]=\"p.coords\" anchor=\"center\">\n            <div class=\"flex flex-col items-center\">\n              <span class=\"size-2.5 rotate-45 rounded-[2px] border border-cyan-400/70 bg-cyan-500/40\"></span>\n              <span\n                class=\"border-border bg-card/80 text-muted-foreground mt-1 rounded border px-1 py-0.5 font-mono text-xs tracking-wider whitespace-nowrap\"\n              >\n                {{ p.name }}\n              </span>\n            </div>\n          </ui-leaflet-marker>\n        }\n\n        <!-- AIS vessels -->\n        @for (v of vessels; track v.mmsi) {\n          <ui-leaflet-marker [lngLat]=\"v.coords\" anchor=\"center\">\n            <div class=\"group relative flex cursor-pointer flex-col items-center\" (click)=\"selectVessel(v)\">\n              <div [class]=\"vesselPinClass(v)\" [style.transform]=\"'rotate(' + v.heading + 'deg)'\">▲</div>\n              <span [class]=\"vesselNameClass(v)\">\n                {{ v.name }}\n              </span>\n            </div>\n          </ui-leaflet-marker>\n        }\n      </ui-leaflet-map>\n    </div>\n\n    <!-- AIS Telemetry Drawer -->\n    <ui-card class=\"flex flex-col justify-between\">\n      <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        <ui-card-title class=\"mt-2 font-mono text-lg\">{{ activeVessel().name }}</ui-card-title>\n        <ui-card-description class=\"font-mono text-xs\"\n          >MMSI: {{ activeVessel().mmsi }} • Flag: {{ activeVessel().flag }}</ui-card-description\n        >\n      </ui-card-header>\n      <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            Rotterdam Approach Traffic\n          </div>\n          @for (v of vessels; track v.mmsi) {\n            <button type=\"button\" [class]=\"rowClass(v)\" (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      </ui-card-content>\n    </ui-card>\n  `,\n})\nexport class UiLeafletMaritimeVesselMapComponent {\n  @Input() vessels: Vessel[] = DEFAULT_VESSELS\n  @Input('class') className?: string\n\n  @ViewChild(UiLeafletMapComponent) map?: UiLeafletMapComponent\n\n  protected readonly mapCenter = MAP_CENTER\n  protected readonly shippingLanes = SHIPPING_LANES\n  protected readonly ports = PORTS\n\n  readonly activeMmsi = signal(DEFAULT_VESSELS[0]?.mmsi ?? '')\n\n  get hostClass(): 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  selectVessel(v: Vessel): void {\n    this.activeMmsi.set(v.mmsi)\n    this.map?.flyTo?.({ center: v.coords, zoom: 11.5, duration: 800 })\n  }\n\n  recenter(): void {\n    this.selectVessel(this.activeVessel())\n  }\n\n  vesselPinClass(v: Vessel): string {\n    return cn(\n      'flex size-7 items-center justify-center rounded-full font-bold text-cyan-400',\n      this.activeMmsi() === v.mmsi ? 'bg-cyan-500/20 ring-2 ring-cyan-400' : 'bg-background/80',\n    )\n  }\n\n  vesselNameClass(v: Vessel): 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() === v.mmsi ? 'border-cyan-400/50 text-cyan-400' : 'text-foreground',\n    )\n  }\n\n  rowClass(v: Vessel): 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() === v.mmsi\n        ? 'bg-accent text-accent-foreground font-semibold'\n        : 'hover:bg-muted text-muted-foreground',\n    )\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/leaflet-maritime-vessel-map/leaflet-maritime-vessel-map.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/leaflet-maritime-vessel-map/index.ts",
      "content": "export {\n  UiLeafletMaritimeVesselMapComponent,\n  MAP_CENTER,\n  DEFAULT_VESSELS,\n  SHIPPING_LANES,\n  PORTS,\n  type Vessel,\n  type VesselType,\n  type VesselStatus,\n  type PortTerminal,\n} from './leaflet-maritime-vessel-map.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/leaflet-maritime-vessel-map/index.ts"
    }
  ],
  "dependencies": [],
  "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/leaflet-map.json"
  ],
  "description": "Automatic Identification System (AIS) vessel tracking on free OpenStreetMap/Esri tiles — no API key. Container ships, tankers, shipping lanes, and anchorage status.",
  "categories": [
    "logistics",
    "maritime",
    "map"
  ]
}