{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "asset-tracking-map",
  "title": "Asset Tracking Map",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/asset-tracking-map/asset-tracking-map.component.ts",
      "content": "// Intermodal cargo container tracking with cold-chain sensor status, geofence state and\n// battery levels on a Mapbox map. Selecting a container (pin or list row) flies the camera\n// there. Port of the React block 1:1; markers follow the hub-locator-map Angular pattern.\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 interface ContainerAsset {\n  id: string\n  containerNo: string\n  type: 'Reefer' | 'Dry Cargo' | 'Hazardous'\n  location: string\n  coords: [number, number]\n  temp?: string\n  humidity?: string\n  battery: string\n  geofence: 'Inside Zone' | 'In Transit' | 'Exited Perimeter'\n  status: 'Nominal' | 'Alert' | 'Secured'\n}\n\nexport const DEFAULT_CONTAINER_ASSETS: ContainerAsset[] = [\n  {\n    id: 'c-1',\n    containerNo: 'MSKU-948102',\n    type: 'Reefer',\n    location: 'Port of Long Beach, Pier G',\n    coords: [-118.216, 33.754],\n    temp: '-18.4°C',\n    humidity: '84%',\n    battery: '92%',\n    geofence: 'Inside Zone',\n    status: 'Nominal',\n  },\n  {\n    id: 'c-2',\n    containerNo: 'CMAU-310892',\n    type: 'Dry Cargo',\n    location: 'Barstow Rail Yard, Track 4',\n    coords: [-117.022, 34.898],\n    battery: '78%',\n    geofence: 'In Transit',\n    status: 'Nominal',\n  },\n  {\n    id: 'c-3',\n    containerNo: 'HLXU-772901',\n    type: 'Reefer',\n    location: 'Inland Empire Logistics Depot',\n    coords: [-117.585, 34.062],\n    temp: '+4.2°C',\n    humidity: '62%',\n    battery: '64%',\n    geofence: 'Inside Zone',\n    status: 'Alert',\n  },\n  {\n    id: 'c-4',\n    containerNo: 'OOLU-550183',\n    type: 'Hazardous',\n    location: 'Pacific Intermodal Terminal',\n    coords: [-118.243, 33.974],\n    battery: '95%',\n    geofence: 'Inside Zone',\n    status: 'Secured',\n  },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-asset-tracking-map, [ui-asset-tracking-map]',\n  standalone: true,\n  imports: [\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiMapComponent,\n  ],\n  host: {\n    'data-slot': 'asset-tracking-map',\n    '[class]': 'hostClass',\n  },\n  template: `\n    <div class=\"grid gap-4 lg:grid-cols-[1fr_320px]\">\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=\"light\"\n          [center]=\"activeAsset().coords\"\n          [zoom]=\"9.2\"\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 (asset of assets; track asset.id) {\n              <div>\n                <div #marker>\n                  <button\n                    type=\"button\"\n                    class=\"group relative flex cursor-pointer flex-col items-center\"\n                    (click)=\"selectAsset(asset)\"\n                  >\n                    <div [class]=\"pinClass(asset.status)\">📦</div>\n                    <span\n                      class=\"border-border bg-background/90 mt-1 rounded border px-1 py-0.5 font-mono text-xs font-bold shadow-xs\"\n                    >\n                      {{ shortNo(asset.containerNo) }}\n                    </span>\n                  </button>\n                </div>\n              </div>\n            }\n          </div>\n        </div>\n      </div>\n\n      <!-- Asset Telemetry & Sensor Card -->\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\">{{ activeAsset().type }}</span>\n            <span ui-badge [class]=\"statusBadgeClass(activeAsset().status)\">{{ activeAsset().status }}</span>\n          </div>\n          <h3 ui-card-title class=\"mt-2 font-mono text-lg\">{{ activeAsset().containerNo }}</h3>\n          <p ui-card-description class=\"text-xs\">{{ activeAsset().location }}</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            @if (activeAsset().temp) {\n              <div>\n                <div class=\"text-muted-foreground text-xs uppercase\">Cold Chain Temp</div>\n                <div [class]=\"tempClass(activeAsset().status)\">{{ activeAsset().temp }}</div>\n              </div>\n            }\n            @if (activeAsset().humidity) {\n              <div>\n                <div class=\"text-muted-foreground text-xs uppercase\">Humidity</div>\n                <div class=\"text-foreground text-sm font-bold\">{{ activeAsset().humidity }}</div>\n              </div>\n            }\n            <div>\n              <div class=\"text-muted-foreground text-xs uppercase\">IoT Battery</div>\n              <div class=\"text-foreground text-sm font-bold\">{{ activeAsset().battery }}</div>\n            </div>\n            <div>\n              <div class=\"text-muted-foreground text-xs uppercase\">Geofence</div>\n              <div class=\"text-foreground text-sm font-bold\">{{ activeAsset().geofence }}</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\">Tracked Cargo Units</div>\n            @for (asset of assets; track asset.id) {\n              <button type=\"button\" [class]=\"unitRowClass(asset.id)\" (click)=\"selectAsset(asset)\">\n                <span class=\"font-mono\">{{ asset.containerNo }}</span>\n                <span [class]=\"unitTypeClass(asset.status)\">{{ asset.type }}</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 Container\n          </button>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiAssetTrackingMapComponent implements OnInit, AfterViewInit, OnDestroy {\n  @Input() assets: ContainerAsset[] = DEFAULT_CONTAINER_ASSETS.map((a) => ({\n    ...a,\n    coords: [...a.coords] as [number, number],\n  }))\n  @Input() accessToken?: string\n  @Input('class') className?: string\n\n  @ViewChildren('marker') private markerEls?: QueryList<ElementRef<HTMLDivElement>>\n\n  readonly activeId = signal('')\n\n  private map: mapboxgl.Map | null = null\n  private markers: mapboxgl.Marker[] = []\n  private destroyed = false\n\n  get hostClass(): string {\n    return cn('block w-full', this.className)\n  }\n\n  ngOnInit(): void {\n    this.activeId.set(this.assets[0]?.id ?? '')\n  }\n\n  ngAfterViewInit(): void {\n    // A new assets 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  activeAsset(): ContainerAsset {\n    return this.assets.find((a) => a.id === this.activeId()) ?? this.assets[0]!\n  }\n\n  shortNo(containerNo: string): string {\n    return containerNo.split('-')[1] ?? containerNo\n  }\n\n  pinClass(status: ContainerAsset['status']): string {\n    return cn(\n      'flex size-7 items-center justify-center rounded-md font-mono text-xs font-bold shadow-md ring-2',\n      status === 'Alert'\n        ? 'bg-warning text-white ring-amber-400'\n        : 'bg-primary text-primary-foreground ring-primary/25',\n    )\n  }\n\n  statusBadgeClass(status: ContainerAsset['status']): string {\n    return status === 'Alert'\n      ? 'border-warning/20 bg-warning/10 text-warning'\n      : 'border-success/20 bg-success/10 text-success'\n  }\n\n  tempClass(status: ContainerAsset['status']): string {\n    return cn('text-sm font-bold', status === 'Alert' ? 'text-warning' : 'text-foreground')\n  }\n\n  unitRowClass(id: 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.activeId() === id\n        ? 'bg-accent text-accent-foreground font-semibold'\n        : 'hover:bg-muted text-muted-foreground',\n    )\n  }\n\n  unitTypeClass(status: ContainerAsset['status']): string {\n    return cn('font-mono text-xs', status === 'Alert' ? 'text-warning font-bold' : 'opacity-70')\n  }\n\n  onMapCreated(map: mapboxgl.Map): void {\n    this.map = map\n    void this.syncMarkers()\n  }\n\n  selectAsset(asset: ContainerAsset): void {\n    this.activeId.set(asset.id)\n    this.map?.flyTo?.({ center: asset.coords, zoom: 11.5 })\n  }\n\n  recenter(): void {\n    const active = this.activeAsset()\n    if (active) this.selectAsset(active)\n  }\n\n  /** One mapbox-gl Marker per container, anchored at the bottom (the React MapMarker). */\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 asset = this.assets[i]\n      if (!asset) return []\n      return [new mapbox.Marker({ element: el.nativeElement, anchor: 'bottom' }).setLngLat(asset.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/asset-tracking-map/asset-tracking-map.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/asset-tracking-map/index.ts",
      "content": "export {\n  DEFAULT_CONTAINER_ASSETS,\n  UiAssetTrackingMapComponent,\n  type ContainerAsset,\n} from './asset-tracking-map.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/asset-tracking-map/index.ts"
    }
  ],
  "dependencies": [
    "mapbox-gl"
  ],
  "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": "Intermodal cargo container tracking with cold-chain sensor status, geofence perimeter rings, and battery levels.",
  "categories": [
    "logistics",
    "supply-chain",
    "map"
  ]
}