{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "disaster-response-map",
  "title": "Disaster Response Map",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/disaster-response-map/disaster-response-map.component.ts",
      "content": "// Incident management map on Mapbox: wildfire hazard perimeter, shelter capacities and\n// emergency staging points with an incident control dashboard. Port of the React block\n// 1:1; markers follow the last-mile-courier-dispatch Angular pattern.\nimport {\n  type AfterViewInit,\n  ChangeDetectionStrategy,\n  Component,\n  type ElementRef,\n  Input,\n  type OnDestroy,\n  type QueryList,\n  ViewChild,\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, type MapLayerInput, type MapSourceInput } from '@/ui/map/map.component'\n\nexport interface ResponseShelter {\n  id: string\n  name: string\n  type: 'Shelter' | 'Medical Staging' | 'Command Post'\n  coords: [number, number]\n  capacity: number\n  occupied: number\n  status: 'Open' | 'Near Capacity' | 'Closed'\n}\n\nexport const HAZARD_CENTER: [number, number] = [-105.28, 40.0]\nexport const MAP_CENTER: [number, number] = [-105.27, 40.01]\nexport const FIRE_ORIGIN: [number, number] = [-105.29, 40.0]\n\n// Simulated wildfire hazard perimeter coordinates (near Boulder, CO)\nexport const FIRE_PERIMETER_COORDS: [number, number][] = [\n  [-105.32, 40.01],\n  [-105.29, 40.03],\n  [-105.26, 40.02],\n  [-105.25, 39.99],\n  [-105.28, 39.97],\n  [-105.31, 39.98],\n  [-105.32, 40.01],\n]\n\nexport const RESPONSE_FACILITIES: ResponseShelter[] = [\n  {\n    id: 's-1',\n    name: 'County Event Center Shelter',\n    type: 'Shelter',\n    coords: [-105.24, 40.04],\n    capacity: 500,\n    occupied: 340,\n    status: 'Open',\n  },\n  {\n    id: 's-2',\n    name: 'West Valley Medical Staging',\n    type: 'Medical Staging',\n    coords: [-105.22, 39.98],\n    capacity: 120,\n    occupied: 98,\n    status: 'Near Capacity',\n  },\n  {\n    id: 's-3',\n    name: 'Unified Incident Command Post',\n    type: 'Command Post',\n    coords: [-105.25, 40.07],\n    capacity: 80,\n    occupied: 45,\n    status: 'Open',\n  },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-disaster-response-map, [ui-disaster-response-map]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"disaster-response-map\"',\n    '[class]': 'hostClass',\n  },\n  imports: [\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiMapComponent,\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          variant=\"light\"\n          [accessToken]=\"accessToken ?? ''\"\n          [center]=\"mapCenter\"\n          [zoom]=\"11.2\"\n          class=\"h-full w-full\"\n          [sources]=\"hazardSources\"\n          [layers]=\"hazardLayers\"\n          (created)=\"onMapCreated($event)\"\n        >\n          <!-- Marker elements: handed to mapbox-gl Markers once the map exists, which moves each\n               inner #fireMarker / #facilityMarker node onto the map; the outer node stays for @for. -->\n          <div class=\"hidden\">\n            <!-- Active Fire Origin Marker -->\n            <div>\n              <div #fireMarker>\n                <div class=\"relative flex cursor-pointer items-center justify-center\" (click)=\"focusHazard()\">\n                  <span class=\"bg-destructive/30 absolute size-9 animate-ping rounded-full\"></span>\n                  <div\n                    class=\"bg-destructive flex size-7 items-center justify-center rounded-full font-mono text-xs font-bold text-white shadow-lg ring-2 ring-white\"\n                  >\n                    🔥\n                  </div>\n                </div>\n              </div>\n            </div>\n\n            <!-- Shelters and Medical Staging -->\n            @for (f of facilities; track f.id) {\n              <div>\n                <div #facilityMarker>\n                  <div class=\"group flex cursor-pointer flex-col items-center\" (click)=\"focusFacility(f)\">\n                    <div\n                      class=\"flex size-7 items-center justify-center rounded-full shadow-md ring-2\"\n                      [class]=\"facilityPinClass(f.type)\"\n                    >\n                      {{ facilityIcon(f.type) }}\n                    </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 whitespace-nowrap shadow-xs\"\n                    >\n                      {{ firstWord(f.name) }}\n                    </span>\n                  </div>\n                </div>\n              </div>\n            }\n          </div>\n        </div>\n      </div>\n\n      <!-- Incident Control Dashboard -->\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\n              ui-badge\n              variant=\"outline\"\n              class=\"border-destructive/30 bg-destructive/10 text-destructive font-mono text-xs\"\n              >LEVEL 3 EVACUATION</span\n            >\n            <span ui-badge class=\"border-success/20 bg-success/10 text-success\">{{ activeFacility().status }}</span>\n          </div>\n          <h3 ui-card-title class=\"mt-2 text-lg\">{{ activeFacility().name }}</h3>\n          <p ui-card-description class=\"text-xs\">\n            Capacity: {{ activeFacility().occupied }} / {{ activeFacility().capacity }} beds occupied\n          </p>\n        </div>\n        <div ui-card-content class=\"space-y-4\">\n          <!-- Capacity Bar -->\n          <div class=\"border-border space-y-1.5 border-t pt-3\">\n            <div class=\"flex items-center justify-between font-mono text-xs\">\n              <span class=\"text-muted-foreground\">Occupancy Rate</span>\n              <span class=\"text-foreground font-bold\">{{ occupancyPercent(activeFacility()) }}%</span>\n            </div>\n            <div class=\"bg-muted h-2 w-full overflow-hidden rounded-full\">\n              <div\n                class=\"h-full rounded-full transition-[width,background-color]\"\n                [class]=\"occupancyBarClass(activeFacility())\"\n                [style.width.%]=\"occupancyPercent(activeFacility())\"\n              ></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\">Staged Relief Sites</div>\n            @for (f of facilities; track f.id) {\n              <button type=\"button\" [class]=\"siteRowClass(f.id)\" (click)=\"focusFacility(f)\">\n                <span class=\"max-w-[170px] truncate\">{{ f.name }}</span>\n                <span class=\"font-mono text-xs opacity-75\">{{ f.occupied }}/{{ f.capacity }}</span>\n              </button>\n            }\n          </div>\n\n          <div class=\"flex gap-2 pt-2\">\n            <button ui-button variant=\"outline\" size=\"sm\" class=\"flex-1 font-mono text-xs\" (click)=\"focusHazard()\">\n              Exclusion Zone\n            </button>\n            <button\n              ui-button\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"flex-1 font-mono text-xs\"\n              (click)=\"focusFacility(activeFacility())\"\n            >\n              Focus Site\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiDisasterResponseMapComponent implements AfterViewInit, OnDestroy {\n  @Input() accessToken?: string\n  @Input('class') className?: string\n\n  @ViewChildren('facilityMarker') private facilityMarkerEls?: QueryList<ElementRef<HTMLDivElement>>\n  @ViewChild('fireMarker') private fireMarkerEl?: ElementRef<HTMLDivElement>\n\n  readonly facilities: ResponseShelter[] = RESPONSE_FACILITIES.map((f) => ({\n    ...f,\n    coords: [...f.coords] as [number, number],\n  }))\n  readonly mapCenter: [number, number] = [...MAP_CENTER] as [number, number]\n  readonly activeId = signal(RESPONSE_FACILITIES[0]?.id ?? '')\n\n  /** The wildfire hazard exclusion zone (the React block's MapSource). */\n  readonly hazardSources: MapSourceInput[] = [\n    {\n      id: 'hazard-zone',\n      type: 'geojson',\n      data: {\n        type: 'Feature',\n        properties: {},\n        geometry: { type: 'Polygon', coordinates: [FIRE_PERIMETER_COORDS.map((c) => [...c])] },\n      },\n    },\n  ]\n\n  /** The red fill + dashed outline (the React block's MapLayers). */\n  readonly hazardLayers: MapLayerInput[] = [\n    {\n      id: 'hazard-zone-fill',\n      type: 'fill',\n      source: 'hazard-zone',\n      paint: { 'fill-color': '#ef4444', 'fill-opacity': 0.28 },\n    },\n    {\n      id: 'hazard-zone-line',\n      type: 'line',\n      source: 'hazard-zone',\n      paint: { 'line-color': '#dc2626', 'line-width': 2.5, 'line-dasharray': [2, 1] },\n    },\n  ]\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  ngAfterViewInit(): void {\n    // A new facilities list re-renders the marker elements; put the new set on the map.\n    this.facilityMarkerEls?.changes.subscribe(() => void this.syncMarkers())\n  }\n\n  ngOnDestroy(): void {\n    this.destroyed = true\n    this.clearMarkers()\n    this.map = null\n  }\n\n  activeFacility(): ResponseShelter {\n    return this.facilities.find((f) => f.id === this.activeId()) ?? this.facilities[0]!\n  }\n\n  occupancyPercent(f: ResponseShelter): number {\n    return Math.round((f.occupied / f.capacity) * 100)\n  }\n\n  occupancyBarClass(f: ResponseShelter): string {\n    return f.occupied / f.capacity > 0.8 ? 'bg-warning' : 'bg-success'\n  }\n\n  facilityIcon(type: ResponseShelter['type']): string {\n    return type === 'Shelter' ? '🏠' : type === 'Medical Staging' ? '✚' : '★'\n  }\n\n  facilityPinClass(type: ResponseShelter['type']): string {\n    return cn(\n      type === 'Shelter'\n        ? 'bg-success text-white ring-emerald-400/40'\n        : type === 'Medical Staging'\n          ? 'bg-info text-white ring-blue-400/40'\n          : 'bg-primary text-primary-foreground ring-primary/40',\n    )\n  }\n\n  siteRowClass(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.activeFacility().id === id\n        ? 'bg-accent text-accent-foreground font-semibold'\n        : 'hover:bg-muted text-muted-foreground',\n    )\n  }\n\n  firstWord(name: string): string {\n    return name.split(' ')[0] ?? name\n  }\n\n  focusFacility(f: ResponseShelter): void {\n    this.activeId.set(f.id)\n    this.map?.flyTo?.({ center: f.coords, zoom: 12.5 })\n  }\n\n  focusHazard(): void {\n    this.map?.flyTo?.({ center: [...HAZARD_CENTER] as [number, number], zoom: 11.5 })\n  }\n\n  onMapCreated(map: mapboxgl.Map): void {\n    this.map = map\n    void this.syncMarkers()\n  }\n\n  /** One mapbox-gl Marker for the fire origin plus one per facility, anchored at the\n      bottom (the React MapMarkers). */\n  private async syncMarkers(): Promise<void> {\n    const map = this.map\n    if (!map) return\n    const facilityEls = this.facilityMarkerEls?.toArray() ?? []\n    const fireEl = this.fireMarkerEl?.nativeElement\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    const facilities = this.facilities\n    this.markers = facilityEls.flatMap((el, i) => {\n      const coords = facilities[i]?.coords\n      if (!coords) return []\n      return [new mapbox.Marker({ element: el.nativeElement, anchor: 'bottom' }).setLngLat(coords).addTo(map)]\n    })\n    if (fireEl) {\n      this.markers.push(new mapbox.Marker({ element: fireEl, anchor: 'center' }).setLngLat([...FIRE_ORIGIN]).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/disaster-response-map/disaster-response-map.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/disaster-response-map/index.ts",
      "content": "export {\n  UiDisasterResponseMapComponent,\n  RESPONSE_FACILITIES,\n  FIRE_PERIMETER_COORDS,\n  FIRE_ORIGIN,\n  HAZARD_CENTER,\n  MAP_CENTER,\n  type ResponseShelter,\n} from './disaster-response-map.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/disaster-response-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": "Incident management map with wildfire hazard perimeter, shelter capacities, and emergency staging points.",
  "categories": [
    "logistics",
    "emergency",
    "map"
  ]
}