{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "isochrone-reachability-map",
  "title": "Isochrone Reachability Map",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/isochrone-reachability-map/isochrone-reachability-map.component.ts",
      "content": "import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'\nimport type * as mapboxgl from 'mapbox-gl'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardDescriptionComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiMapComponent, UiMapMarkerComponent, type MapLayerInput, type MapSourceInput } from '@/ui/map/map.component'\n\nexport type IsochroneTime = 10 | 20 | 30\n\ninterface IsochroneZone {\n  coords: [number, number][]\n  pop: string\n  jobs: string\n  color: string\n  label: string\n}\n\nconst HUB_LOCATION: [number, number] = [-87.6298, 41.8781] // Chicago Loop\nconst HUB_LAT = 41.8781\nconst TIME_OPTIONS: IsochroneTime[] = [10, 20, 30]\n\n// Approximated polygon coordinates for 10m, 20m, 30m isochrones\nfunction createIsochroneCoords(radiusMiles: number): [number, number][] {\n  const points: [number, number][] = []\n  const steps = 36\n  const latDelta = radiusMiles / 69\n  const lngDelta = radiusMiles / (69 * Math.cos((HUB_LAT * Math.PI) / 180))\n  for (let i = 0; i <= steps; i++) {\n    const angle = (i / steps) * 2 * Math.PI\n    // Add small organic jitter\n    const jitter = 1 + Math.sin(angle * 3) * 0.12\n    const lng = HUB_LOCATION[0] + lngDelta * Math.cos(angle) * jitter\n    const lat = HUB_LOCATION[1] + latDelta * Math.sin(angle) * jitter\n    points.push([lng, lat])\n  }\n  return points\n}\n\nfunction buildZones(): Record<IsochroneTime, IsochroneZone> {\n  return {\n    10: { coords: createIsochroneCoords(2.8), pop: '340K', jobs: '520K', color: '#10b981', label: '10 Min Commute' },\n    20: { coords: createIsochroneCoords(6.2), pop: '1.24M', jobs: '890K', color: '#3b82f6', label: '20 Min Commute' },\n    30: { coords: createIsochroneCoords(11.5), pop: '2.85M', jobs: '1.45M', color: '#f59e0b', label: '30 Min Commute' },\n  }\n}\n\nconst ISOCHRONES = buildZones()\n\nfunction polygonSource(id: string, coords: [number, number][]): MapSourceInput {\n  return {\n    id,\n    type: 'geojson',\n    data: {\n      type: 'Feature',\n      properties: {},\n      geometry: { type: 'Polygon', coordinates: [coords] },\n    },\n  }\n}\n\n/**\n * IsochroneReachabilityMap -- the React block, 1:1: drive-time reachability zones showing\n * 10m, 20m, and 30m commute polygons on a Mapbox map with a workforce analytics card\n * (COMMUTE ANALYSIS, Drive-Time, Reachability Zones, Transit reachability from Downtown\n * Distribution Hub) and a 10/20/30 min range selector driving population, labor pool and\n * service band metrics. Sources are static GeoJSON; layer fill emphasis follows the\n * selected time, live-patched onto the map when it exists.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-isochrone-reachability-map, [ui-isochrone-reachability-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    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiMapComponent,\n    UiMapMarkerComponent,\n  ],\n  template: `\n    <div data-slot=\"isochrone-reachability-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=\"light\"\n          [center]=\"hubLocation\"\n          [zoom]=\"10.8\"\n          class=\"h-full w-full\"\n          [sources]=\"mapSources\"\n          [layers]=\"mapLayers()\"\n          (created)=\"onMapCreated($event)\"\n        >\n          <!-- Central Hub Marker -->\n          <ui-map-marker [lngLat]=\"hubLocation\" anchor=\"center\">\n            <div class=\"relative flex items-center justify-center\">\n              <span class=\"bg-primary/30 absolute size-6 animate-ping rounded-full\"></span>\n              <div\n                class=\"bg-primary text-primary-foreground ring-background flex size-6 items-center justify-center rounded-full font-mono text-xs font-bold shadow-md ring-2\"\n              >\n                ★\n              </div>\n            </div>\n          </ui-map-marker>\n        </div>\n      </div>\n\n      <!-- Isochrone Analytics 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\">COMMUTE ANALYSIS</span>\n            <span ui-badge class=\"border-info/20 bg-info/10 text-info\">Drive-Time</span>\n          </div>\n          <h3 ui-card-title class=\"mt-2 text-lg\">Reachability Zones</h3>\n          <p ui-card-description>Transit reachability from Downtown Distribution Hub.</p>\n        </div>\n        <div ui-card-content class=\"space-y-4\">\n          <!-- Range Selector Tabs -->\n          <div class=\"border-border bg-muted grid grid-cols-3 gap-1 rounded-lg border p-1 text-xs\">\n            @for (t of timeOptions; track t) {\n              <button\n                type=\"button\"\n                class=\"rounded-md py-1.5 font-medium transition-colors\"\n                [class]=\"\n                  selectedTime() === t\n                    ? 'bg-background text-foreground shadow-xs'\n                    : 'text-muted-foreground hover:text-foreground'\n                \"\n                (click)=\"selectTime(t)\"\n              >\n                {{ t }} mins\n              </button>\n            }\n          </div>\n\n          <div class=\"border-border space-y-3 border-t pt-3\">\n            <div class=\"flex items-center justify-between\">\n              <span class=\"text-muted-foreground text-xs\">Population Reach</span>\n              <span class=\"text-foreground font-mono text-base font-bold\">{{ activeZone().pop }}</span>\n            </div>\n            <div class=\"flex items-center justify-between\">\n              <span class=\"text-muted-foreground text-xs\">Workforce / Labor Pool</span>\n              <span class=\"text-foreground font-mono text-base font-bold\">{{ activeZone().jobs }}</span>\n            </div>\n            <div class=\"flex items-center justify-between\">\n              <span class=\"text-muted-foreground text-xs\">Service Band</span>\n              <span class=\"font-mono text-xs font-semibold\" [style.color]=\"activeZone().color\">\n                {{ activeZone().label }}\n              </span>\n            </div>\n          </div>\n\n          <div class=\"border-border text-muted-foreground space-y-2 border-t pt-3 text-xs\">\n            <div class=\"flex items-center gap-2\">\n              <span class=\"bg-success size-2 rounded-full\"></span>\n              <span>Green ring: 10-minute urban core</span>\n            </div>\n            <div class=\"flex items-center gap-2\">\n              <span class=\"bg-info size-2 rounded-full\"></span>\n              <span>Blue ring: 20-minute metropolitan belt</span>\n            </div>\n            <div class=\"flex items-center gap-2\">\n              <span class=\"bg-warning size-2 rounded-full\"></span>\n              <span>Amber ring: 30-minute outer ring</span>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiIsochroneReachabilityMapComponent {\n  protected readonly timeOptions = TIME_OPTIONS\n  protected readonly hubLocation = HUB_LOCATION\n\n  @Input() accessToken = ''\n  @Input('class') className?: string\n\n  readonly selectedTime = signal<IsochroneTime>(20)\n\n  readonly activeZone = computed<IsochroneZone>(() => ISOCHRONES[this.selectedTime()])\n\n  /** Static GeoJSON polygons: 30m outer ring first so the 10m core paints on top. */\n  readonly mapSources: MapSourceInput[] = (TIME_OPTIONS.slice().reverse() as IsochroneTime[]).map((t) =>\n    polygonSource(`iso-${t}`, ISOCHRONES[t].coords),\n  )\n\n  /** Fill emphasis follows the selected time; lines stay constant. */\n  readonly mapLayers = computed<MapLayerInput[]>(() => {\n    const selected = this.selectedTime()\n    const layers: MapLayerInput[] = []\n    for (const t of TIME_OPTIONS.slice().reverse() as IsochroneTime[]) {\n      const zone = ISOCHRONES[t]\n      const fillOpacity = t === 10 ? 0.3 : t === 20 ? (selected >= 20 ? 0.22 : 0.06) : selected >= 30 ? 0.15 : 0.04\n      layers.push({\n        id: `iso-${t}-fill`,\n        type: 'fill',\n        source: `iso-${t}`,\n        paint: { 'fill-color': zone.color, 'fill-opacity': fillOpacity },\n      })\n      layers.push({\n        id: `iso-${t}-line`,\n        type: 'line',\n        source: `iso-${t}`,\n        paint:\n          t === 30\n            ? { 'line-color': zone.color, 'line-width': 1.5, 'line-dasharray': [2, 2] }\n            : { 'line-color': zone.color, 'line-width': t === 10 ? 2.5 : 2 },\n      })\n    }\n    return layers\n  })\n\n  private map: mapboxgl.Map | null = null\n\n  get rootClass(): string {\n    return cn('grid gap-4 lg:grid-cols-[1fr_320px]', this.className)\n  }\n\n  selectTime(t: IsochroneTime): void {\n    this.selectedTime.set(t)\n    // The map only adds missing layers on input change, so patch live paint directly.\n    try {\n      this.map?.setPaintProperty('iso-30-fill', 'fill-opacity', t >= 30 ? 0.15 : 0.04)\n      this.map?.setPaintProperty('iso-20-fill', 'fill-opacity', t >= 20 ? 0.22 : 0.06)\n    } catch {\n      /* map not ready yet; the computed layers cover first paint */\n    }\n  }\n\n  onMapCreated(map: mapboxgl.Map): void {\n    this.map = map\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/isochrone-reachability-map/isochrone-reachability-map.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/isochrone-reachability-map/index.ts",
      "content": "export { UiIsochroneReachabilityMapComponent, type IsochroneTime } from './isochrone-reachability-map.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/isochrone-reachability-map/index.ts"
    }
  ],
  "dependencies": [
    "mapbox-gl"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/map.json"
  ],
  "description": "Drive-time reachability zones showing 10m, 20m, and 30m commute polygons with workforce population metrics.",
  "categories": [
    "logistics",
    "geo",
    "map"
  ]
}