{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "leaflet-isochrone-reachability-map",
  "title": "Leaflet Isochrone Reachability Map",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/leaflet-isochrone-reachability-map/leaflet-isochrone-reachability-map.component.ts",
      "content": "// Drive-time reachability zones showing 10m, 20m, and 30m commute polygons with workforce\n// population metrics — on free OpenStreetMap/Esri tiles, no API key. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'\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 {\n  UiLeafletMapComponent,\n  UiLeafletMarkerComponent,\n  UiLeafletPolygonComponent,\n} from '@/ui/leaflet-map/leaflet-map.component'\n\nexport type IsochroneMinutes = 10 | 20 | 30\n\nexport interface IsochroneBand {\n  coords: [number, number][]\n  pop: string\n  jobs: string\n  color: string\n  label: string\n}\n\nexport const HUB_LOCATION: [number, number] = [-122.419, 37.775] // Downtown San Francisco\n\n// Key-free drive-time approximation: a perturbed ring whose radius varies by\n// angle through fixed harmonic offsets, so each band reads as an irregular\n// blob rather than a perfect circle (a real isochrone needs a routing API).\nexport function createIsochroneCoords(radiusMiles: number, phase: number): [number, number][] {\n  const points: [number, number][] = []\n  const steps = 48\n  const latDelta = radiusMiles / 69\n  const lngDelta = radiusMiles / (69 * Math.cos((HUB_LOCATION[1] * Math.PI) / 180))\n  for (let i = 0; i <= steps; i++) {\n    const angle = (i / steps) * 2 * Math.PI\n    const jitter = 1 + 0.16 * Math.sin(angle * 3 + phase) + 0.07 * Math.sin(angle * 7 + phase * 2.3)\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\nexport const ISOCHRONES: Record<IsochroneMinutes, IsochroneBand> = {\n  10: {\n    coords: createIsochroneCoords(2.8, 0.4),\n    pop: '340K',\n    jobs: '520K',\n    color: '#10b981',\n    label: '10 Min Commute',\n  },\n  20: {\n    coords: createIsochroneCoords(6.2, 1.7),\n    pop: '1.24M',\n    jobs: '890K',\n    color: '#3b82f6',\n    label: '20 Min Commute',\n  },\n  30: {\n    coords: createIsochroneCoords(11.5, 2.9),\n    pop: '2.85M',\n    jobs: '1.45M',\n    color: '#f59e0b',\n    label: '30 Min Commute',\n  },\n}\n\nexport interface IsochroneDestination {\n  name: string\n  lngLat: [number, number]\n}\n\nexport const DESTINATIONS: IsochroneDestination[] = [\n  { name: 'Mission Depot', lngLat: [-122.414, 37.762] },\n  { name: 'Daly City Hub', lngLat: [-122.47, 37.705] },\n  { name: 'Oakland Annex', lngLat: [-122.272, 37.804] },\n]\n\nconst TIME_OPTIONS: IsochroneMinutes[] = [10, 20, 30]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-leaflet-isochrone-reachability-map, [ui-leaflet-isochrone-reachability-map]',\n  standalone: true,\n  imports: [\n    UiBadgeComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiLeafletMapComponent,\n    UiLeafletMarkerComponent,\n    UiLeafletPolygonComponent,\n  ],\n  host: {\n    'data-slot': 'leaflet-isochrone-reachability-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=\"light\" [center]=\"hubLocation\" [zoom]=\"10.5\" class=\"absolute inset-0 size-full\">\n        <!-- 30m Isochrone (outermost, drawn first) -->\n        <ui-leaflet-polygon\n          [lngLatPath]=\"isochrones[30].coords\"\n          [color]=\"isochrones[30].color\"\n          [weight]=\"1.5\"\n          dashArray=\"4 4\"\n          [fill]=\"true\"\n          [fillColor]=\"isochrones[30].color\"\n          [fillOpacity]=\"selectedTime() >= 30 ? 0.15 : 0.04\"\n        />\n\n        <!-- 20m Isochrone -->\n        <ui-leaflet-polygon\n          [lngLatPath]=\"isochrones[20].coords\"\n          [color]=\"isochrones[20].color\"\n          [weight]=\"2\"\n          [fill]=\"true\"\n          [fillColor]=\"isochrones[20].color\"\n          [fillOpacity]=\"selectedTime() >= 20 ? 0.22 : 0.06\"\n        />\n\n        <!-- 10m Isochrone (innermost, most opaque) -->\n        <ui-leaflet-polygon\n          [lngLatPath]=\"isochrones[10].coords\"\n          [color]=\"isochrones[10].color\"\n          [weight]=\"2.5\"\n          [fill]=\"true\"\n          [fillColor]=\"isochrones[10].color\"\n          [fillOpacity]=\"0.3\"\n        />\n\n        <!-- Destinations -->\n        @for (dest of destinations; track dest.name) {\n          <ui-leaflet-marker [lngLat]=\"dest.lngLat\" anchor=\"center\">\n            <div class=\"flex flex-col items-center gap-0.5\">\n              <span class=\"border-background bg-foreground size-2 rounded-full border-2 shadow-xs\"></span>\n              <span\n                class=\"bg-card/90 text-foreground rounded px-1 font-mono text-xs font-semibold whitespace-nowrap shadow-xs\"\n              >\n                {{ dest.name }}\n              </span>\n            </div>\n          </ui-leaflet-marker>\n        }\n\n        <!-- Central Hub Marker -->\n        <ui-leaflet-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-leaflet-marker>\n      </ui-leaflet-map>\n    </div>\n\n    <!-- Isochrone Analytics Card -->\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\">COMMUTE ANALYSIS</span>\n          <span ui-badge class=\"border-info/20 bg-info/10 text-info\">Drive-Time</span>\n        </div>\n        <ui-card-title class=\"mt-2 text-lg\">Reachability Zones</ui-card-title>\n        <ui-card-description>Transit reachability from Downtown Distribution Hub.</ui-card-description>\n      </ui-card-header>\n      <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 type=\"button\" [class]=\"timeTabClass(t)\" (click)=\"selectedTime.set(t)\">{{ t }} mins</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\">{{ activeBand().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\">{{ activeBand().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]=\"activeBand().color\">\n              {{ activeBand().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      </ui-card-content>\n    </ui-card>\n  `,\n})\nexport class UiLeafletIsochroneReachabilityMapComponent {\n  @Input('class') className?: string\n\n  protected readonly hubLocation = HUB_LOCATION\n  protected readonly isochrones = ISOCHRONES\n  protected readonly destinations = DESTINATIONS\n  protected readonly timeOptions = TIME_OPTIONS\n\n  readonly selectedTime = signal<IsochroneMinutes>(20)\n\n  get hostClass(): string {\n    return cn('grid gap-4 lg:grid-cols-[1fr_320px]', this.className)\n  }\n\n  activeBand(): IsochroneBand {\n    return this.isochrones[this.selectedTime()]\n  }\n\n  timeTabClass(t: IsochroneMinutes): string {\n    return cn(\n      'rounded-md py-1.5 font-medium transition-colors',\n      this.selectedTime() === t\n        ? 'bg-background text-foreground shadow-xs'\n        : 'text-muted-foreground hover:text-foreground',\n    )\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/leaflet-isochrone-reachability-map/leaflet-isochrone-reachability-map.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/leaflet-isochrone-reachability-map/index.ts",
      "content": "export {\n  UiLeafletIsochroneReachabilityMapComponent,\n  HUB_LOCATION,\n  ISOCHRONES,\n  DESTINATIONS,\n  createIsochroneCoords,\n  type IsochroneBand,\n  type IsochroneDestination,\n  type IsochroneMinutes,\n} from './leaflet-isochrone-reachability-map.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/leaflet-isochrone-reachability-map/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/leaflet-map.json"
  ],
  "description": "Drive-time reachability zones showing 10m, 20m, and 30m commute polygons with workforce population metrics — on free OpenStreetMap/Esri tiles, no API key.",
  "categories": [
    "logistics",
    "geo",
    "map"
  ]
}