{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "map",
  "title": "Map",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/map/map.component.ts",
      "content": "import {\n  AfterViewInit,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  SimpleChanges,\n  ViewChild,\n  ChangeDetectionStrategy,\n  DOCUMENT,\n  inject,\n} from '@angular/core'\nimport type * as mapboxgl from 'mapbox-gl'\nimport { cn } from '@/lib/utils'\nimport { mapVariants, MAPBOX_STYLES, type MapVariant, type MapVariants } from './map.variants'\n\nexport type MapSize = NonNullable<MapVariants['size']>\nexport type MapNavigationPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n\nexport interface MapSourceInput {\n  id: string\n  type?: string\n  data?: unknown\n  options?: mapboxgl.AnySourceData\n}\n\nexport interface MapLayerInput {\n  id: string\n  type?: string\n  source?: string\n  paint?: Record<string, unknown>\n  layout?: Record<string, unknown>\n  beforeId?: string\n  options?: Partial<mapboxgl.AnyLayer>\n}\n\n/** The design-system Mapbox chrome — `map.css` beside this file, verbatim (a spec keeps them in sync). */\nexport const MAP_CSS = `\n.mapboxgl-ctrl-group {\n  background: var(--card);\n  border: 1px solid var(--border);\n  border-radius: var(--radius);\n  box-shadow: var(--shadow-sm, 0 1px 2px rgb(0 0 0 / 0.08));\n}\n.mapboxgl-ctrl-group button {\n  background: transparent;\n}\n.mapboxgl-ctrl-group button + button {\n  border-top-color: var(--border);\n}\n.mapboxgl-ctrl-group button .mapboxgl-ctrl-icon {\n  filter: invert(var(--map-ctrl-invert, 0)) opacity(0.7);\n}\n.dark .mapboxgl-ctrl-group {\n  background: var(--card, #18181b);\n  border-color: var(--border, #27272a);\n}\n.dark .mapboxgl-ctrl-group button {\n  background: transparent;\n}\n.dark .mapboxgl-ctrl-group button:hover {\n  background: var(--muted, #27272a);\n}\n.dark .mapboxgl-ctrl-group button + button {\n  border-top-color: var(--border, #27272a);\n}\n.dark .mapboxgl-ctrl-group button .mapboxgl-ctrl-icon {\n  filter: invert(1) brightness(1.2);\n}\n.mapboxgl-ctrl-attrib {\n  background: color-mix(in oklab, var(--card) 80%, transparent);\n  color: var(--muted-foreground);\n}\n.mapboxgl-ctrl-attrib a {\n  color: var(--muted-foreground);\n}\n.mapboxgl-popup-content {\n  padding: 8px 12px;\n  background: var(--popover);\n  color: var(--popover-foreground);\n  border: 1px solid var(--border);\n  border-radius: var(--radius);\n  box-shadow: var(--shadow-md, 0 4px 12px rgb(0 0 0 / 0.12));\n}\n.mapboxgl-popup-close-button {\n  color: var(--muted-foreground);\n}\n.mapboxgl-popup-tip {\n  border-top-color: var(--popover);\n  border-bottom-color: var(--popover);\n}\n`\n\n/**\n * Puts `MAP_CSS` into `<head>` once. It is global on purpose: Mapbox renders its\n * controls and popups outside the component's view, so encapsulated styles can't reach them.\n */\nexport function ensureMapCss(doc: Document): void {\n  if (doc.head.querySelector('style[data-uipkge-map]')) return\n  const style = doc.createElement('style')\n  style.setAttribute('data-uipkge-map', '')\n  style.textContent = MAP_CSS\n  doc.head.appendChild(style)\n}\n\n/**\n * Loads Mapbox GL's own stylesheet (what React/Vue get from `import 'mapbox-gl/dist/mapbox-gl.css'`)\n * unless the page already has it. Without it markers are `position: static` and stack below the canvas.\n *\n * Detection is by effect, not by URL: a probe `.mapboxgl-marker` that computes to `position: absolute`\n * means the stylesheet is present (e.g. listed in angular.json `styles`), so nothing is added.\n * Otherwise a `<link>` to Mapbox's CDN copy for the exact installed version is appended once.\n */\nexport function ensureMapboxCss(doc: Document, version: string | undefined): void {\n  if (!version || doc.head.querySelector('link[data-uipkge-mapbox]')) return\n  const probe = doc.createElement('div')\n  probe.className = 'mapboxgl-marker'\n  doc.body.appendChild(probe)\n  const present = doc.defaultView?.getComputedStyle(probe).position === 'absolute'\n  probe.remove()\n  if (present) return\n  const link = doc.createElement('link')\n  link.rel = 'stylesheet'\n  link.href = `https://api.mapbox.com/mapbox-gl-js/v${version}/mapbox-gl.css`\n  link.setAttribute('data-uipkge-mapbox', '')\n  doc.head.appendChild(link)\n}\n\ntype MapboxModule = typeof import('mapbox-gl')\n\nasync function loadMapbox(doc: Document): Promise<MapboxModule['default']> {\n  const mod = await import('mapbox-gl')\n  const mapbox = (mod.default ?? mod) as MapboxModule['default']\n  ensureMapboxCss(doc, (mapbox as { version?: string }).version)\n  return mapbox\n}\n\n/**\n * Angular port of the UIPKGE Map (Mapbox GL JS). Standalone.\n * Class strings come from shared `mapVariants` — identical to Vue/React.\n *\n * Mapbox GL touches `window`/`document`, so the module is loaded lazily on\n * the client only (`await import('mapbox-gl')` inside `ngAfterViewInit`).\n * Provide `accessToken`; without a token the host renders only a\n * \"token required\" placeholder (no map element), exactly like React.\n *\n * Styles: nothing to import. The first `<ui-map>` puts the design-system chrome\n * (`map.css`, shipped beside this file) into `<head>`, and on first map creation\n * Mapbox GL's own stylesheet is linked from Mapbox's CDN for the installed\n * version. To self-host it instead (CSP, offline), add\n * `node_modules/mapbox-gl/dist/mapbox-gl.css` to `styles` in angular.json;\n * the component detects it and skips the CDN link.\n *\n * Markers: drop `<ui-map-marker [lngLat]=\"[lng, lat]\" anchor=\"center\">` with any\n * HTML inside into the content slot (React/Vue `MapMarker`).\n *\n * Declarative `sources`/`layers` inputs mirror the `<ui-map-source>` /\n * `<ui-map-layer>` slot children for consumers that prefer data over slots.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-map, [ui-map]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"map\"',\n    '[attr.data-variant]': 'variant',\n    '[attr.data-muted]': 'isMuted',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @if (hasToken) {\n      <div #mapEl class=\"size-full\"></div>\n    } @else {\n      <div\n        class=\"text-muted-foreground flex size-full flex-col items-center justify-center gap-1 px-6 text-center text-sm\"\n      >\n        <span class=\"text-foreground font-medium\">Mapbox token required</span>\n        <span>Pass an access token to render the map.</span>\n      </div>\n    }\n    <ng-content />\n  `,\n})\nexport class UiMapComponent implements AfterViewInit, OnChanges, OnDestroy {\n  @Input() accessToken = ''\n  @Input() variant: MapVariant = 'default'\n  @Input() size?: MapSize\n  /** Override the base style. Defaults to the variant or a theme-aware light/dark style. */\n  @Input() mapStyle?: string\n  /** Alias for `mapStyle` (matches the \"style\" input shorthand). */\n  @Input() style?: string\n  /** Initial [lng, lat]. */\n  @Input() center: [number, number] = [0, 20]\n  @Input() zoom = 1.4\n  @Input() pitch = 0\n  @Input() bearing = 0\n  @Input() buildings3d = false\n  @Input() terrain3d = false\n  @Input() projection = 'mercator'\n  @Input() navigation = true\n  @Input() navigationPosition: MapNavigationPosition = 'bottom-right'\n  @Input() showCompass?: boolean\n  @Input() showZoom = true\n  @Input() fullscreen = false\n  @Input() fullscreenPosition: MapNavigationPosition = 'top-right'\n  /** Repaint the basemap to a quiet, desaturated canvas. */\n  @Input() muted = false\n  /** Declarative sources (alternative to `<ui-map-source>` slot children). */\n  @Input() sources: MapSourceInput[] = []\n  /** Declarative layers (alternative to `<ui-map-layer>` slot children). */\n  @Input() layers: MapLayerInput[] = []\n  @Input('class') className?: string\n\n  /** Emits the raw mapbox-gl Map once the style is ready. */\n  @Output() created = new EventEmitter<mapboxgl.Map>()\n\n  mapEl?: ElementRef<HTMLDivElement>\n  /** The map element only exists with a token; a token that arrives later creates the map then. */\n  @ViewChild('mapEl', { static: false }) set mapElRef(el: ElementRef<HTMLDivElement> | undefined) {\n    this.mapEl = el\n    if (!el || !this.viewReady) return\n    this.resizeObserver?.observe(el.nativeElement)\n    if (!this.map) void this.createMap()\n  }\n\n  private viewReady = false\n  private mapListeners: Array<(map: mapboxgl.Map) => void> = []\n\n  private map: mapboxgl.Map | null = null\n  private navControl: mapboxgl.NavigationControl | null = null\n  private fullscreenControl: mapboxgl.FullscreenControl | null = null\n  private themeObserver: MutationObserver | null = null\n  private resizeObserver: ResizeObserver | null = null\n  private dark = false\n  private inited = false\n\n  /** Calls `cb` with the mapbox-gl Map as soon as it exists (before the style loads, like react-map-gl children). */\n  whenMap(cb: (map: mapboxgl.Map) => void): () => void {\n    if (this.map) {\n      cb(this.map)\n      return () => {}\n    }\n    this.mapListeners.push(cb)\n    return () => {\n      this.mapListeners = this.mapListeners.filter((l) => l !== cb)\n    }\n  }\n\n  get isMuted(): boolean {\n    return this.muted || this.variant === 'muted'\n  }\n\n  get hasToken(): boolean {\n    return this.resolveToken().length > 0\n  }\n\n  get hostClass(): string {\n    return cn(\n      'block',\n      mapVariants({ variant: this.variant, ...(this.size ? { size: this.size } : {}) }),\n      this.className,\n    )\n  }\n\n  resolveToken(): string {\n    return this.accessToken\n  }\n\n  /** Resolve the style URL: explicit `style`/`mapStyle` wins, then the variant preset, then theme-aware light/dark. */\n  resolveStyle(): string {\n    const override = this.style ?? this.mapStyle\n    if (override) return override\n    if (this.variant && this.variant !== 'default' && this.variant !== 'muted') {\n      const preset = MAPBOX_STYLES[this.variant as keyof typeof MAPBOX_STYLES]\n      if (preset) return preset\n    }\n    return this.dark ? 'mapbox://styles/mapbox/dark-v11' : 'mapbox://styles/mapbox/light-v11'\n  }\n\n  /** The underlying mapbox-gl Map, or null until created. */\n  getMap(): mapboxgl.Map | null {\n    return this.map\n  }\n\n  flyTo(...args: Parameters<mapboxgl.Map['flyTo']>): unknown {\n    return this.map?.flyTo(...args)\n  }\n\n  easeTo(...args: Parameters<mapboxgl.Map['easeTo']>): unknown {\n    return this.map?.easeTo(...args)\n  }\n\n  jumpTo(...args: Parameters<mapboxgl.Map['jumpTo']>): unknown {\n    return this.map?.jumpTo(...args)\n  }\n\n  fitBounds(...args: Parameters<mapboxgl.Map['fitBounds']>): unknown {\n    return this.map?.fitBounds(...args)\n  }\n\n  resize(): void {\n    this.map?.resize()\n  }\n\n  async ngAfterViewInit(): Promise<void> {\n    if (typeof document === 'undefined' || typeof window === 'undefined') return\n    // Client-only (like mapbox-gl itself), so `new UiMapComponent()` stays usable outside DI.\n    ensureMapCss(document)\n    const root = document.documentElement\n    const syncTheme = () => {\n      this.dark = root.classList.contains('dark')\n    }\n    syncTheme()\n    this.themeObserver = new MutationObserver(syncTheme)\n    this.themeObserver.observe(root, { attributes: true, attributeFilter: ['class'] })\n    this.viewReady = true\n    if (typeof ResizeObserver !== 'undefined') {\n      this.resizeObserver = new ResizeObserver(() => this.map?.resize())\n      if (this.mapEl?.nativeElement) this.resizeObserver.observe(this.mapEl.nativeElement)\n    }\n    if (!this.hasToken || !this.mapEl?.nativeElement) return\n    await this.createMap()\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (!this.map || !this.inited) return\n    if (changes['pitch'] && this.pitch !== undefined) {\n      try {\n        this.map.setPitch(this.pitch)\n      } catch {\n        /* ignore */\n      }\n    }\n    if (changes['bearing'] && this.bearing !== undefined) {\n      try {\n        this.map.setBearing(this.bearing)\n      } catch {\n        /* ignore */\n      }\n    }\n    if (changes['navigation'] || changes['navigationPosition'] || changes['showCompass'] || changes['showZoom']) {\n      void this.updateNavControl()\n    }\n    if (changes['fullscreen'] || changes['fullscreenPosition']) {\n      void this.updateFullscreenControl()\n    }\n    if ((changes['sources'] || changes['layers']) && this.map.isStyleLoaded()) {\n      this.applySourcesAndLayers()\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.resizeObserver?.disconnect()\n    this.resizeObserver = null\n    this.themeObserver?.disconnect()\n    this.themeObserver = null\n    try {\n      this.map?.remove()\n    } catch {\n      /* already destroyed */\n    }\n    this.map = null\n    this.mapListeners = []\n  }\n\n  private async createMap(): Promise<void> {\n    const el = this.mapEl?.nativeElement\n    if (!el || this.map) return\n    const mapbox = await loadMapbox(el.ownerDocument)\n    if (this.map || !this.mapEl) return\n    mapbox.accessToken = this.resolveToken()\n    const map = new mapbox.Map({\n      container: el,\n      style: this.resolveStyle(),\n      center: this.center,\n      zoom: this.zoom,\n      attributionControl: false,\n    })\n    this.map = map as unknown as mapboxgl.Map\n    const listeners = this.mapListeners\n    this.mapListeners = []\n    for (const cb of listeners) cb(this.map)\n    this.map.resize()\n    try {\n      ;(this.map as unknown as { setProjection: (p: string) => void }).setProjection(this.projection)\n    } catch {\n      /* older api */\n    }\n    if (this.pitch) {\n      try {\n        this.map.setPitch(this.pitch)\n      } catch {\n        /* ignore */\n      }\n    }\n    if (this.bearing) {\n      try {\n        this.map.setBearing(this.bearing)\n      } catch {\n        /* ignore */\n      }\n    }\n    await this.updateNavControl()\n    await this.updateFullscreenControl()\n    if (!this.map) return\n    const ready = () => {\n      if (this.isMuted) this.applyMuted()\n      if (this.buildings3d) this.apply3DBuildings()\n      if (this.terrain3d) this.apply3DTerrain()\n      this.applySourcesAndLayers()\n      this.inited = true\n      if (this.map) this.created.emit(this.map)\n    }\n    if (this.map.isStyleLoaded()) ready()\n    else this.map.once('load', ready)\n    this.map.on('style.load', () => {\n      if (this.isMuted) this.applyMuted()\n      if (this.buildings3d) this.apply3DBuildings()\n      if (this.terrain3d) this.apply3DTerrain()\n      this.applySourcesAndLayers()\n    })\n  }\n\n  private async updateNavControl(): Promise<void> {\n    if (!this.map) return\n    const mod = await import('mapbox-gl')\n    const mapbox = mod.default ?? mod\n    if (!this.map) return\n    if (this.navControl) {\n      try {\n        this.map.removeControl(this.navControl)\n      } catch {\n        /* ignore */\n      }\n      this.navControl = null\n    }\n    if (this.navigation) {\n      const showCompass = this.showCompass !== undefined ? this.showCompass : (this.pitch ?? 0) > 0\n      this.navControl = new mapbox.NavigationControl({ showCompass, showZoom: this.showZoom ?? true })\n      this.map.addControl(this.navControl, this.navigationPosition ?? 'bottom-right')\n    }\n  }\n\n  private async updateFullscreenControl(): Promise<void> {\n    if (!this.map) return\n    const mod = await import('mapbox-gl')\n    const mapbox = mod.default ?? mod\n    if (!this.map) return\n    if (this.fullscreenControl) {\n      try {\n        this.map.removeControl(this.fullscreenControl)\n      } catch {\n        /* ignore */\n      }\n      this.fullscreenControl = null\n    }\n    if (this.fullscreen) {\n      this.fullscreenControl = new mapbox.FullscreenControl()\n      this.map.addControl(this.fullscreenControl, this.fullscreenPosition ?? 'top-right')\n    }\n  }\n\n  /** Desaturate the basemap to a quiet canvas. Each op is guarded — style layer ids drift between style versions. */\n  private applyMuted(): void {\n    const map = this.map\n    if (!map) return\n    let styleLayers: Array<{ id: string; type: string }>\n    try {\n      styleLayers = (map.getStyle()?.layers as Array<{ id: string; type: string }>) ?? []\n    } catch {\n      return\n    }\n    const P = this.dark\n      ? {\n          land: 'rgb(24,24,27)',\n          water: 'rgb(39,39,42)',\n          use: 'rgb(30,30,34)',\n          road: 'rgb(45,46,52)',\n          label: 'rgb(161,161,170)',\n          halo: 'rgb(24,24,27)',\n          admin: 'rgb(50,52,60)',\n        }\n      : {\n          land: 'rgb(246,247,249)',\n          water: 'rgb(226,230,235)',\n          use: 'rgb(238,240,243)',\n          road: 'rgb(221,224,229)',\n          label: 'rgb(120,124,134)',\n          halo: 'rgb(246,247,249)',\n          admin: 'rgb(213,216,221)',\n        }\n    for (const l of styleLayers) {\n      const id = l.id\n      try {\n        if (id === 'background' || id === 'land') map.setPaintProperty(id, 'background-color', P.land)\n        else if (/water/.test(id) && l.type === 'fill') map.setPaintProperty(id, 'fill-color', P.water)\n        else if (/landuse|landcover|national-park/.test(id) && l.type === 'fill')\n          map.setPaintProperty(id, 'fill-color', P.use)\n        else if (/^road-(motorway|trunk|primary)/.test(id) && l.type === 'line') {\n          map.setPaintProperty(id, 'line-color', P.road)\n          map.setPaintProperty(id, 'line-opacity', 0.55)\n        } else if (/^road-(secondary|tertiary|street|minor|service|path|pedestrian)/.test(id)) {\n          map.setLayoutProperty(id, 'visibility', 'none')\n        } else if (/poi|transit|airport|natural-point|water-point|waterway-label|building/.test(id)) {\n          map.setLayoutProperty(id, 'visibility', 'none')\n        } else if (\n          /road-label|settlement-major-label|settlement-minor-label|state-label/.test(id) &&\n          l.type === 'symbol'\n        ) {\n          map.setPaintProperty(id, 'text-color', P.label)\n          map.setPaintProperty(id, 'text-halo-color', P.halo)\n          map.setPaintProperty(id, 'text-opacity', 0.6)\n        } else if (id === 'admin-1-boundary' && l.type === 'line') {\n          map.setPaintProperty(id, 'line-color', P.admin)\n          map.setPaintProperty(id, 'line-opacity', 0.6)\n        } else if (id === 'admin-1-boundary-bg') map.setPaintProperty(id, 'line-opacity', 0)\n      } catch {\n        /* skip */\n      }\n    }\n  }\n\n  private apply3DBuildings(): void {\n    const map = this.map\n    if (!map || !this.buildings3d || map.getLayer('3d-buildings')) return\n    const layers = map.getStyle()?.layers\n    let labelLayerId: string | undefined\n    if (layers) {\n      for (const layer of layers) {\n        if (layer.type === 'symbol' && (layer.layout as Record<string, unknown> | undefined)?.['text-field']) {\n          labelLayerId = layer.id\n          break\n        }\n      }\n    }\n    try {\n      map.addLayer(\n        {\n          id: '3d-buildings',\n          source: 'composite',\n          'source-layer': 'building',\n          filter: ['==', 'extrude', 'true'],\n          type: 'fill-extrusion',\n          minzoom: 14,\n          paint: {\n            'fill-extrusion-color': this.dark ? '#27272a' : '#d4d4d8',\n            'fill-extrusion-height': ['interpolate', ['linear'], ['zoom'], 14, 0, 14.05, ['get', 'height']],\n            'fill-extrusion-base': ['interpolate', ['linear'], ['zoom'], 14, 0, 14.05, ['get', 'min_height']],\n            'fill-extrusion-opacity': 0.8,\n          },\n        },\n        labelLayerId,\n      )\n    } catch {\n      /* building layer not present in raster styles */\n    }\n  }\n\n  private apply3DTerrain(): void {\n    const map = this.map\n    if (!map || !this.terrain3d) return\n    try {\n      if (!map.getSource('mapbox-dem')) {\n        map.addSource('mapbox-dem', {\n          type: 'raster-dem',\n          url: 'mapbox://mapbox.mapbox-terrain-dem-v1',\n          tileSize: 512,\n          maxzoom: 14,\n        })\n      }\n      map.setTerrain({ source: 'mapbox-dem', exaggeration: 1.5 })\n    } catch {\n      /* terrain not supported in current projection */\n    }\n  }\n\n  /** Apply the declarative `sources`/`layers` inputs onto the live map. No-ops until the style is loaded. */\n  applySourcesAndLayers(): void {\n    const map = this.map\n    if (!map) return\n    try {\n      if (!map.isStyleLoaded()) return\n    } catch {\n      return\n    }\n    for (const s of this.sources ?? []) {\n      try {\n        if (!map.getSource(s.id)) map.addSource(s.id, UiMapSourceComponent.buildSourceOptions(s))\n      } catch {\n        /* retry on next style.load */\n      }\n    }\n    for (const l of this.layers ?? []) {\n      try {\n        if (!map.getLayer(l.id)) map.addLayer(UiMapLayerComponent.buildLayerConfig(l), l.beforeId)\n      } catch {\n        /* retry on next style.load */\n      }\n    }\n  }\n}\n\nexport { mapVariants, MAPBOX_STYLES, type MapVariant, type MapVariants }\n\n/**\n * Declarative GeoJSON (or any) source for `<ui-map>`. Slot children\n * (`<ui-map-layer>`) inherit this source id.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-map-source, [ui-map-source]',\n  standalone: true,\n  host: {\n    '[attr.data-map-source-id]': 'id',\n    '[attr.data-uipkge]': '\"\"',\n  },\n  template: `<ng-content />`,\n})\nexport class UiMapSourceComponent {\n  @Input() id!: string\n  @Input() type = 'geojson'\n  @Input() data?: unknown\n  @Input() options?: mapboxgl.AnySourceData\n\n  static buildSourceOptions(s: {\n    type?: string\n    data?: unknown\n    options?: mapboxgl.AnySourceData\n  }): mapboxgl.AnySourceData {\n    if (s.options) return s.options\n    return { type: (s.type || 'geojson') as 'geojson', data: s.data } as mapboxgl.AnySourceData\n  }\n\n  getSourceOptions(): mapboxgl.AnySourceData {\n    return UiMapSourceComponent.buildSourceOptions({ type: this.type, data: this.data, options: this.options })\n  }\n}\n\n/**\n * Declarative layer for `<ui-map>` / `<ui-map-source>`. When `options` is\n * given it carries its own `type`; otherwise the flat `type` input is used.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-map-layer, [ui-map-layer]',\n  standalone: true,\n  host: {\n    '[attr.data-map-layer-id]': 'id',\n    '[attr.data-uipkge]': '\"\"',\n  },\n  template: `<ng-content />`,\n})\nexport class UiMapLayerComponent {\n  @Input() id!: string\n  @Input() type?: string\n  @Input() source?: string\n  @Input() paint: Record<string, unknown> = {}\n  @Input() layout: Record<string, unknown> = {}\n  @Input() beforeId?: string\n  @Input() options?: Partial<mapboxgl.AnyLayer>\n\n  static buildLayerConfig(l: {\n    id: string\n    type?: string\n    source?: string\n    paint?: Record<string, unknown>\n    layout?: Record<string, unknown>\n    options?: Partial<mapboxgl.AnyLayer>\n  }): mapboxgl.AnyLayer {\n    if (l.options) return { ...l.options, id: l.id } as mapboxgl.AnyLayer\n    return {\n      id: l.id,\n      type: l.type,\n      source: l.source,\n      paint: l.paint || {},\n      layout: l.layout || {},\n    } as unknown as mapboxgl.AnyLayer\n  }\n\n  getLayerConfig(): mapboxgl.AnyLayer {\n    return UiMapLayerComponent.buildLayerConfig({\n      id: this.id,\n      type: this.type,\n      source: this.source,\n      paint: this.paint,\n      layout: this.layout,\n      options: this.options,\n    })\n  }\n}\n\nexport type MapMarkerAnchor = mapboxgl.Anchor\n\n/**\n * HTML marker for `<ui-map>` (React/Vue `MapMarker`). Its content is handed to a\n * mapbox-gl `Marker` once the parent map exists, which moves that element onto the\n * map; the host itself stays hidden where it was declared.\n *\n *   <ui-map-marker [lngLat]=\"[-98.5, 39.8]\" anchor=\"center\"><button>NA</button></ui-map-marker>\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-map-marker, [ui-map-marker]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"map-marker\"',\n    '[attr.data-uipkge]': '\"\"',\n    class: 'hidden',\n  },\n  template: `<div #markerEl><ng-content /></div>`,\n})\nexport class UiMapMarkerComponent implements AfterViewInit, OnChanges, OnDestroy {\n  /** [lng, lat] — matches React/Vue `lngLat`. */\n  @Input({ required: true }) lngLat!: [number, number]\n  /** Which part of the element sits on `lngLat`. Mapbox's default is 'center'. */\n  @Input() anchor: MapMarkerAnchor = 'center'\n\n  @ViewChild('markerEl', { static: true }) private markerEl!: ElementRef<HTMLDivElement>\n\n  private readonly parent = inject(UiMapComponent, { optional: true })\n  private readonly doc = inject(DOCUMENT)\n  private marker: mapboxgl.Marker | null = null\n  private map: mapboxgl.Map | null = null\n  private stopWaiting: (() => void) | null = null\n  private destroyed = false\n\n  /** The mapbox-gl Marker, or null until the map exists. */\n  getMarker(): mapboxgl.Marker | null {\n    return this.marker\n  }\n\n  ngAfterViewInit(): void {\n    this.stopWaiting = this.parent?.whenMap((map) => void this.attach(map)) ?? null\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (!this.marker) return\n    if (changes['anchor'] && this.map) {\n      void this.attach(this.map)\n    } else if (changes['lngLat'] && this.lngLat) {\n      this.marker.setLngLat(this.lngLat)\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.destroyed = true\n    this.stopWaiting?.()\n    this.marker?.remove()\n    this.marker = null\n    this.map = null\n  }\n\n  private async attach(map: mapboxgl.Map): Promise<void> {\n    const mapbox = await loadMapbox(this.doc)\n    if (this.destroyed) return\n    this.marker?.remove()\n    this.map = map\n    this.marker = new mapbox.Marker({ element: this.markerEl.nativeElement, anchor: this.anchor })\n      .setLngLat(this.lngLat)\n      .addTo(map)\n  }\n}\n\nexport type MapPopupAnchor = mapboxgl.Anchor\n\n/**\n * Popup dialog for `<ui-map>` (React/Vue `MapPopup`). Its content is handed to a\n * mapbox-gl `Popup` once the parent map exists, which moves that element onto the\n * map; the host itself stays hidden where it was declared. Render conditionally\n * (`@if (active)`) like React's `{active && <MapPopup>}` for selection popups.\n *\n *   <ui-map-marker [lngLat]=\"hq\" anchor=\"bottom\">…</ui-map-marker>\n *   @if (active === 'hq') {\n *     <ui-map-popup [lngLat]=\"hq\" [offset]=\"[0, -32]\" (close)=\"active = null\">…</ui-map-popup>\n *   }\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-map-popup, [ui-map-popup]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"map-popup\"',\n    '[attr.data-uipkge]': '\"\"',\n    class: 'hidden',\n  },\n  template: `<div #popupEl><ng-content /></div>`,\n})\nexport class UiMapPopupComponent implements AfterViewInit, OnChanges, OnDestroy {\n  /** [lng, lat] — matches React/Vue `lngLat`. */\n  @Input({ required: true }) lngLat!: [number, number]\n  @Input() anchor?: MapPopupAnchor\n  @Input() offset?: [number, number] | number\n  @Input() closeButton?: boolean\n  @Input() closeOnClick?: boolean\n  @Input() closeOnMove?: boolean\n  @Input() maxWidth?: string\n  /** Extra class for the popup content (React `className`). */\n  @Input() className?: string\n  /** Fired when the popup closes (React `onClose` / Vue `@close`). */\n  @Output() close = new EventEmitter<void>()\n\n  @ViewChild('popupEl', { static: true }) private popupEl!: ElementRef<HTMLDivElement>\n\n  private readonly parent = inject(UiMapComponent, { optional: true })\n  private readonly doc = inject(DOCUMENT)\n  private popup: mapboxgl.Popup | null = null\n  private map: mapboxgl.Map | null = null\n  private stopWaiting: (() => void) | null = null\n  private destroyed = false\n\n  /** The mapbox-gl Popup, or null until the map exists. */\n  getPopup(): mapboxgl.Popup | null {\n    return this.popup\n  }\n\n  buildOptions(): mapboxgl.PopupOptions {\n    const opts: Record<string, unknown> = {\n      anchor: this.anchor,\n      offset: this.offset,\n      closeButton: this.closeButton,\n      closeOnClick: this.closeOnClick,\n      closeOnMove: this.closeOnMove,\n      maxWidth: this.maxWidth,\n      className: this.className,\n    }\n    return Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== undefined)) as mapboxgl.PopupOptions\n  }\n\n  ngAfterViewInit(): void {\n    this.stopWaiting = this.parent?.whenMap((map) => void this.attach(map)) ?? null\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (!this.popup) return\n    const optionKeys = ['anchor', 'offset', 'closeButton', 'closeOnClick', 'closeOnMove', 'maxWidth', 'className']\n    if (optionKeys.some((k) => changes[k]) && this.map) {\n      void this.attach(this.map)\n    } else if (changes['lngLat'] && this.lngLat) {\n      this.popup.setLngLat(this.lngLat)\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.destroyed = true\n    this.stopWaiting?.()\n    this.popup?.remove()\n    this.popup = null\n    this.map = null\n  }\n\n  private async attach(map: mapboxgl.Map): Promise<void> {\n    const mapbox = await loadMapbox(this.doc)\n    if (this.destroyed) return\n    this.popup?.remove()\n    this.map = map\n    this.popup = new mapbox.Popup(this.buildOptions())\n      .setLngLat(this.lngLat)\n      .setDOMContent(this.popupEl.nativeElement)\n      .addTo(map)\n    this.popup.on('close', () => this.close.emit())\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/map/map.component.ts"
    },
    {
      "path": "packages/registry-angular/components/map/map.variants.ts",
      "content": "import { cva, type VariantProps } from 'class-variance-authority'\n\nexport type MapVariant =\n  | 'default'\n  | 'muted'\n  | 'streets'\n  | 'outdoors'\n  | 'light'\n  | 'dark'\n  | 'satellite'\n  | 'satellite-streets'\n  | 'navigation-day'\n  | 'navigation-night'\n  | 'standard'\n\nexport const MAPBOX_STYLES: Record<Exclude<MapVariant, 'default' | 'muted'>, string> = {\n  streets: 'mapbox://styles/mapbox/streets-v12',\n  outdoors: 'mapbox://styles/mapbox/outdoors-v12',\n  light: 'mapbox://styles/mapbox/light-v11',\n  dark: 'mapbox://styles/mapbox/dark-v11',\n  satellite: 'mapbox://styles/mapbox/satellite-v9',\n  'satellite-streets': 'mapbox://styles/mapbox/satellite-streets-v12',\n  'navigation-day': 'mapbox://styles/mapbox/navigation-day-v1',\n  'navigation-night': 'mapbox://styles/mapbox/navigation-night-v1',\n  standard: 'mapbox://styles/mapbox/standard',\n}\n\nexport const mapVariants = cva('relative size-full overflow-hidden bg-muted', {\n  variants: {\n    variant: {\n      default: '',\n      muted: 'data-[muted=true]:grayscale-[25%]',\n      streets: '',\n      outdoors: '',\n      light: '',\n      dark: '',\n      satellite: '',\n      'satellite-streets': '',\n      'navigation-day': '',\n      'navigation-night': '',\n      standard: '',\n    },\n    size: {\n      default: 'h-96 w-full',\n      sm: 'h-64 w-full',\n      lg: 'h-[500px] w-full',\n      xl: 'h-[650px] w-full',\n      full: 'size-full',\n    },\n  },\n  defaultVariants: {\n    variant: 'default',\n  },\n})\n\nexport type MapVariants = VariantProps<typeof mapVariants>\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/map/map.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/map/index.ts",
      "content": "export {\n  UiMapComponent,\n  UiMapSourceComponent,\n  UiMapLayerComponent,\n  UiMapMarkerComponent,\n  UiMapPopupComponent,\n  MAP_CSS,\n  ensureMapCss,\n  ensureMapboxCss,\n  type MapMarkerAnchor,\n  type MapPopupAnchor,\n  mapVariants,\n  MAPBOX_STYLES,\n  type MapVariant,\n  type MapVariants,\n  type MapSize,\n  type MapNavigationPosition,\n  type MapSourceInput,\n  type MapLayerInput,\n} from './map.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/map/index.ts"
    },
    {
      "path": "packages/registry-angular/components/map/map.css",
      "content": "/**\n * Map chrome — re-skins Mapbox GL's controls + popups to the design-system\n * tokens so they sit naturally on light and dark surfaces. Global (Mapbox\n * renders this chrome outside the component subtree). Import once, e.g. in your\n * root CSS: `@import './components/ui/map/map.css';`\n */\n.mapboxgl-ctrl-group {\n  background: var(--card);\n  border: 1px solid var(--border);\n  border-radius: var(--radius);\n  box-shadow: var(--shadow-sm, 0 1px 2px rgb(0 0 0 / 0.08));\n}\n.mapboxgl-ctrl-group button {\n  background: transparent;\n}\n.mapboxgl-ctrl-group button + button {\n  border-top-color: var(--border);\n}\n.mapboxgl-ctrl-group button .mapboxgl-ctrl-icon {\n  filter: invert(var(--map-ctrl-invert, 0)) opacity(0.7);\n}\n.dark .mapboxgl-ctrl-group {\n  background: var(--card, #18181b);\n  border-color: var(--border, #27272a);\n}\n.dark .mapboxgl-ctrl-group button {\n  background: transparent;\n}\n.dark .mapboxgl-ctrl-group button:hover {\n  background: var(--muted, #27272a);\n}\n.dark .mapboxgl-ctrl-group button + button {\n  border-top-color: var(--border, #27272a);\n}\n.dark .mapboxgl-ctrl-group button .mapboxgl-ctrl-icon {\n  filter: invert(1) brightness(1.2);\n}\n.mapboxgl-ctrl-attrib {\n  background: color-mix(in oklab, var(--card) 80%, transparent);\n  color: var(--muted-foreground);\n}\n.mapboxgl-ctrl-attrib a {\n  color: var(--muted-foreground);\n}\n.mapboxgl-popup-content {\n  padding: 8px 12px;\n  background: var(--popover);\n  color: var(--popover-foreground);\n  border: 1px solid var(--border);\n  border-radius: var(--radius);\n  box-shadow: var(--shadow-md, 0 4px 12px rgb(0 0 0 / 0.12));\n}\n.mapboxgl-popup-close-button {\n  color: var(--muted-foreground);\n}\n.mapboxgl-popup-tip {\n  border-top-color: var(--popover);\n  border-bottom-color: var(--popover);\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/map/map.css"
    }
  ],
  "dependencies": [
    "mapbox-gl",
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "A thin, theme-aware Mapbox GL JS wrapper as Angular standalone components (UiMapComponent + UiMapMarkerComponent + UiMapSourceComponent + UiMapLayerComponent). Pass an access token and drop ui-map-marker / ui-map-source / ui-map-layer into the content slot to build any map. The base style follows light/dark automatically, an opt-in `muted` input desaturates the basemap, and `created` hands you the raw map instance. The design-system map chrome (map.css) and Mapbox GL's stylesheet load automatically; add mapbox-gl/dist/mapbox-gl.css to angular.json styles to self-host the latter.",
  "categories": [
    "data-display"
  ]
}