{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "leaflet-maritime-vessel-map",
  "title": "Leaflet Maritime Vessel Map",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/leaflet-maritime-vessel-map/LeafletMaritimeVesselMap.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed, ref } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { LeafletMap, LeafletMarker, LeafletPolyline, type LeafletMapRef } from '@/components/ui/leaflet-map'\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\nexport interface LeafletMaritimeVesselMapProps {\n  class?: HTMLAttributes['class']\n  vessels?: Vessel[]\n}\n\nconst props = defineProps<LeafletMaritimeVesselMapProps>()\n\n// Module-level constant: a stable reference keeps LeafletMap's `center` watcher\n// from re-firing setView on every re-render and cancelling flyTo animations.\nconst MAP_CENTER: [number, number] = [4.4, 51.9]\n\nconst 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.\nconst 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\nconst PORTS: { name: string; coords: [number, number] }[] = [\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\nconst vessels = computed(() => props.vessels ?? DEFAULT_VESSELS)\nconst activeMmsi = ref(DEFAULT_VESSELS[0]?.mmsi ?? '')\nconst activeVessel = computed(() => vessels.value.find((v) => v.mmsi === activeMmsi.value) ?? vessels.value[0])\n\nconst mapRef = ref<LeafletMapRef | null>(null)\n\nfunction selectVessel(v: Vessel) {\n  activeMmsi.value = v.mmsi\n  mapRef.value?.flyTo?.({ center: v.coords, zoom: 11.5, duration: 800 })\n}\n</script>\n\n<template>\n  <div data-slot=\"leaflet-maritime-vessel-map\" :class=\"cn('grid gap-4 lg:grid-cols-[1fr_320px]', props.class)\">\n    <div class=\"border-border bg-card relative h-[480px] overflow-hidden rounded-xl border\">\n      <LeafletMap ref=\"mapRef\" variant=\"dark\" :center=\"MAP_CENTER\" :zoom=\"11\" class=\"h-full w-full\">\n        <!-- Shipping lanes -->\n        <LeafletPolyline\n          v-for=\"(lane, i) in SHIPPING_LANES\"\n          :key=\"i\"\n          :lng-lat-path=\"lane\"\n          color=\"#22d3ee\"\n          :weight=\"2\"\n          :opacity=\"0.45\"\n          dash-array=\"6 6\"\n          line-cap=\"round\"\n        />\n\n        <!-- Port terminals -->\n        <LeafletMarker v-for=\"p in PORTS\" :key=\"p.name\" :lng-lat=\"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\" />\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        </LeafletMarker>\n\n        <!-- AIS vessels -->\n        <LeafletMarker v-for=\"v in vessels\" :key=\"v.mmsi\" :lng-lat=\"v.coords\" anchor=\"center\" @click=\"selectVessel(v)\">\n          <div class=\"group relative flex cursor-pointer flex-col items-center\">\n            <div\n              class=\"flex size-7 items-center justify-center rounded-full font-bold text-cyan-400\"\n              :class=\"activeMmsi === v.mmsi ? 'bg-cyan-500/20 ring-2 ring-cyan-400' : 'bg-background/80'\"\n              :style=\"{ transform: `rotate(${v.heading}deg)` }\"\n            >\n              ▲\n            </div>\n            <span\n              class=\"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              :class=\"activeMmsi === v.mmsi ? 'border-cyan-400/50 text-cyan-400' : 'text-foreground'\"\n            >\n              {{ v.name }}\n            </span>\n          </div>\n        </LeafletMarker>\n      </LeafletMap>\n    </div>\n\n    <!-- AIS Telemetry Drawer -->\n    <Card class=\"flex flex-col justify-between\">\n      <CardHeader>\n        <div class=\"flex items-center justify-between\">\n          <Badge variant=\"outline\" class=\"font-mono text-xs\">{{ activeVessel.type }}</Badge>\n          <Badge class=\"border-cyan-500/20 bg-cyan-500/10 text-cyan-600 dark:text-cyan-400\">{{\n            activeVessel.status\n          }}</Badge>\n        </div>\n        <CardTitle class=\"mt-2 font-mono text-lg\">{{ activeVessel.name }}</CardTitle>\n        <CardDescription class=\"font-mono text-xs\"\n          >MMSI: {{ activeVessel.mmsi }} • Flag: {{ activeVessel.flag }}</CardDescription\n        >\n      </CardHeader>\n      <CardContent 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          <button\n            v-for=\"v in vessels\"\n            :key=\"v.mmsi\"\n            type=\"button\"\n            class=\"flex w-full items-center justify-between rounded-md px-2 py-1 text-xs transition-colors\"\n            :class=\"\n              activeMmsi === v.mmsi\n                ? 'bg-accent text-accent-foreground font-semibold'\n                : 'hover:bg-muted text-muted-foreground'\n            \"\n            @click=\"selectVessel(v)\"\n          >\n            <span class=\"font-mono\">{{ v.name }}</span>\n            <span class=\"font-mono text-xs opacity-75\">{{ v.speedKnots }} kts</span>\n          </button>\n        </div>\n\n        <Button variant=\"outline\" size=\"sm\" class=\"w-full font-mono text-xs\" @click=\"selectVessel(activeVessel)\">\n          Center on Vessel\n        </Button>\n      </CardContent>\n    </Card>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/LeafletMaritimeVesselMap.vue"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/card.json",
    "https://uipkge.dev/r/vue/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"
  ]
}