{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "sales-territory-map",
  "title": "Sales Territory Map",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/sales-territory-map/sales-territory-map.component.ts",
      "content": "// Sales territory map. Region pins with quota attainment on a Mapbox map, and a\n// quota list beside it; selecting a territory (pin or row) flies the camera there.\nimport {\n  type AfterViewInit,\n  Component,\n  type ElementRef,\n  Input,\n  type OnDestroy,\n  type OnInit,\n  type QueryList,\n  ViewChildren,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport type * as mapboxgl from 'mapbox-gl'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiMapComponent } from '@/ui/map/map.component'\n\nexport interface SalesTerritory {\n  id: string\n  name: string\n  code: string\n  arr: string\n  attainment: number\n  status: 'attained' | 'ontrack' | 'attention'\n  centerLngLat: [number, number]\n}\n\nexport const DEFAULT_TERRITORIES: SalesTerritory[] = [\n  {\n    id: 'na-enterprise',\n    name: 'North America',\n    code: 'NA-ENT',\n    arr: '$6.4M',\n    attainment: 111,\n    status: 'attained',\n    centerLngLat: [-98.5, 39.8],\n  },\n  {\n    id: 'emea-central',\n    name: 'EMEA Central',\n    code: 'EMEA-C',\n    arr: '$5.8M',\n    attainment: 124,\n    status: 'attained',\n    centerLngLat: [10.4, 51.1],\n  },\n  {\n    id: 'apac-growth',\n    name: 'APAC Growth',\n    code: 'APAC-G',\n    arr: '$4.1M',\n    attainment: 94,\n    status: 'ontrack',\n    centerLngLat: [120.0, 30.0],\n  },\n  {\n    id: 'latam-emerging',\n    name: 'Latin America',\n    code: 'LATAM-E',\n    arr: '$2.1M',\n    attainment: 83,\n    status: 'attention',\n    centerLngLat: [-55.0, -14.0],\n  },\n]\n\nfunction statusVariant(status: SalesTerritory['status']): BadgeVariant {\n  if (status === 'attained') return 'info'\n  if (status === 'ontrack') return 'secondary'\n  return 'warning'\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-sales-territory-map, [ui-sales-territory-map]',\n  standalone: true,\n  imports: [\n    UiBadgeComponent,\n    UiCardComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiCardContentComponent,\n    UiMapComponent,\n  ],\n  host: {\n    'data-slot': 'sales-territory-map',\n    '[class]': 'hostClass',\n  },\n  template: `\n    <div class=\"flex flex-wrap items-center justify-between gap-3\">\n      <h2 class=\"text-foreground text-lg font-semibold tracking-tight\">Territories</h2>\n      <p class=\"text-muted-foreground text-xs\">{{ territories.length }} regions</p>\n    </div>\n\n    <div class=\"grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-12\">\n      <div ui-card class=\"border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-8\">\n        <div class=\"relative min-h-[360px] flex-1 overflow-hidden\">\n          <div\n            ui-map\n            [accessToken]=\"accessToken ?? ''\"\n            variant=\"dark\"\n            [center]=\"[15, 20]\"\n            [zoom]=\"1.2\"\n            class=\"absolute inset-0 size-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 (t of territories; track t.id) {\n                <div>\n                  <div #marker>\n                    <button type=\"button\" [class]=\"markerClass(t.id)\" (click)=\"selectTerritory(t)\">\n                      {{ t.code }} {{ t.attainment }}%\n                    </button>\n                  </div>\n                </div>\n              }\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div ui-card class=\"border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-4\">\n        <div ui-card-header class=\"border-border border-b p-4\">\n          <h3 ui-card-title class=\"text-sm font-semibold\">Quota</h3>\n        </div>\n        <div ui-card-content class=\"divide-border min-h-0 flex-1 divide-y overflow-auto p-0\">\n          @for (t of territories; track t.id) {\n            <button type=\"button\" [class]=\"rowClass(t.id)\" (click)=\"selectTerritory(t)\">\n              <div class=\"min-w-0\">\n                <p class=\"text-foreground truncate text-sm font-medium\">{{ t.code }} · {{ t.name }}</p>\n                <p class=\"text-muted-foreground font-mono text-xs tabular-nums\">{{ t.arr }}</p>\n              </div>\n              <span ui-badge [variant]=\"statusVariant(t.status)\" class=\"shrink-0 text-xs tabular-nums\">\n                {{ t.attainment }}%\n              </span>\n            </button>\n          }\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiSalesTerritoryMapComponent implements OnInit, AfterViewInit, OnDestroy {\n  @Input() territories: SalesTerritory[] = DEFAULT_TERRITORIES\n  @Input() accessToken?: string\n  @Input('class') className?: string\n\n  @ViewChildren('marker') private markerEls?: QueryList<ElementRef<HTMLDivElement>>\n\n  readonly selectedId = signal('')\n  protected readonly statusVariant = statusVariant\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('flex h-full min-h-0 flex-col gap-4', this.className)\n  }\n\n  ngOnInit(): void {\n    this.selectedId.set(this.territories[0]?.id ?? '')\n  }\n\n  ngAfterViewInit(): void {\n    // A new territories 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  markerClass(id: string): string {\n    return this.selectedId() === id\n      ? 'border-primary bg-primary text-primary-foreground rounded-full border px-2 py-0.5 font-mono text-xs font-semibold shadow-xs'\n      : 'border-border bg-card text-foreground rounded-full border px-2 py-0.5 font-mono text-xs font-semibold shadow-xs'\n  }\n\n  rowClass(id: string): string {\n    return cn(\n      'hover:bg-accent/50 flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors',\n      this.selectedId() === id && 'bg-accent/60',\n    )\n  }\n\n  onMapCreated(map: mapboxgl.Map): void {\n    this.map = map\n    if (typeof map.loaded === 'function' && map.loaded()) this.fitTerritories(map)\n    else map.once('load', () => this.fitTerritories(map))\n    void this.syncMarkers()\n  }\n\n  selectTerritory(territory: SalesTerritory): void {\n    this.selectedId.set(territory.id)\n    this.map?.flyTo({\n      center: territory.centerLngLat,\n      zoom: 2.8,\n      essential: true,\n      duration: 800,\n    })\n  }\n\n  private fitTerritories(map: mapboxgl.Map): void {\n    const coords = this.territories.map((t) => t.centerLngLat)\n    if (coords.length < 2) return\n    const lons = coords.map((c) => c[0])\n    const lats = coords.map((c) => c[1])\n    try {\n      map.fitBounds(\n        [\n          [Math.min(...lons), Math.min(...lats)],\n          [Math.max(...lons), Math.max(...lats)],\n        ],\n        { padding: { top: 48, bottom: 48, left: 80, right: 80 }, duration: 0, maxZoom: 1.8 },\n      )\n    } catch {\n      /* map not ready */\n    }\n  }\n\n  /** One mapbox-gl Marker per territory, anchored at its centre (the React MapMarker, anchor=\"center\"). */\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 t = this.territories[i]\n      if (!t) return []\n      return [new mapbox.Marker({ element: el.nativeElement, anchor: 'center' }).setLngLat(t.centerLngLat).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/sales-territory-map/sales-territory-map.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/sales-territory-map/index.ts",
      "content": "export { UiSalesTerritoryMapComponent, DEFAULT_TERRITORIES, type SalesTerritory } from './sales-territory-map.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/sales-territory-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": "Sales territory map. Region pins with quota attainment; select a territory to fly the camera.",
  "categories": [
    "analytics",
    "dashboard"
  ]
}