{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "map",
  "title": "Map",
  "type": "registry:ui",
  "description": "A thin, theme-aware Mapbox GL JS wrapper (built on react-map-gl). Pass an access token and drop MapMarker / MapPopup / MapLayer into the children to build any map — fleet boards, journey maps, store locators. The base style follows light/dark automatically, an opt-in `muted` prop desaturates the basemap so overlaid data is the only colour, and `onCreated` hands you the raw map instance for custom layers and fitBounds. Import `mapbox-gl/dist/mapbox-gl.css` once in your app.",
  "categories": [
    "data-display"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/map/map.css",
      "content": "/**\n * Map chrome — React's map.css (packages/registry-react/components/map/map.css)\n * adopted into <uip-map>'s shadow root, where Mapbox renders its controls and\n * popups. Vendor-override CSS: Mapbox's own DOM can't take utility classes.\n * Only change from React: `.dark …` can't see <html class=\"dark\"> from inside\n * a shadow root, so it is `:host([data-theme='dark']) …` (set by ThemeController).\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:host([data-theme='dark']) .mapboxgl-ctrl-group {\n  background: var(--card, #18181b);\n  border-color: var(--border, #27272a);\n}\n:host([data-theme='dark']) .mapboxgl-ctrl-group button {\n  background: transparent;\n}\n:host([data-theme='dark']) .mapboxgl-ctrl-group button:hover {\n  background: var(--muted, #27272a);\n}\n:host([data-theme='dark']) .mapboxgl-ctrl-group button + button {\n  border-top-color: var(--border, #27272a);\n}\n:host([data-theme='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:file",
      "target": "src/uip/components/map/map.css"
    },
    {
      "path": "packages/elements-lit/src/components/map/map.ts",
      "content": "import { LitElement, css, html, isServer, unsafeCSS, type PropertyValues } from 'lit'\nimport type mapboxgl from 'mapbox-gl'\nimport mapboxCss from 'mapbox-gl/dist/mapbox-gl.css?inline'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport mapCss from './map.css?inline'\nimport { mapVariants, MAPBOX_STYLES, type MapVariant, type MapVariants } from './map.variants'\n\ntype Mapbox = typeof mapboxgl\ntype Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\ntype LngLat = [number, number]\n\n/** `true` unless the attribute is absent or the string \"false\" — so defaults\n *  of `true` (navigation, show-zoom, close-button…) can be turned off in HTML. */\nconst boolAttr = { fromAttribute: (v: string | null) => v !== null && v !== 'false', toAttribute: (v: boolean) => (v ? '' : null) }\n/** Tri-state: absent → undefined (React's \"auto\"), \"false\" → false, else true. */\nconst optionalBool = { fromAttribute: (v: string | null) => (v === null ? undefined : v !== 'false') }\n/** JSON when it looks like JSON (`[0, 12]`, `{…}`), otherwise a number. */\nconst numberOrJson = { fromAttribute: (v: string | null) => (v === null ? undefined : /^\\s*[[{]/.test(v) ? JSON.parse(v) : Number(v)) }\n\nconst isDarkPage = () => !isServer && document.documentElement.classList.contains('dark')\n\n// ── Basemap helpers (verbatim from React's map.tsx) ───────────────────────────\n\n/** Desaturate the basemap to a quiet canvas. Each op is guarded — style layer\n *  ids drift between style versions and the wrong property for a layer type\n *  throws. */\nfunction applyMuted(map: mapboxgl.Map, dark: boolean) {\n  let styleLayers: any[]\n  try {\n    styleLayers = map.getStyle()?.layers ?? []\n  } catch {\n    return // style not loaded yet\n  }\n  const P = dark\n    ? {\n        land: 'rgb(23,24,29)',\n        water: 'rgb(17,18,22)',\n        use: 'rgb(31,32,38)',\n        road: 'rgb(50,52,60)',\n        label: 'rgb(150,152,165)',\n        halo: 'rgb(23,24,29)',\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\nfunction apply3DBuildings(map: mapboxgl.Map, dark: boolean) {\n  if (map.getLayer('3d-buildings')) return\n  const layers = map.getStyle()?.layers\n  let labelLayerId: string | undefined\n  if (layers) {\n    for (let i = 0; i < layers.length; i++) {\n      if (layers[i].type === 'symbol' && (layers[i].layout as any)?.['text-field']) {\n        labelLayerId = layers[i].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': 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\nfunction apply3DTerrain(map: mapboxgl.Map) {\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/** Resolve `lng-lat` or `longitude` + `latitude` (React accepts both). */\nfunction coordsOf(el: { lngLat?: LngLat; longitude?: number; latitude?: number }): LngLat | undefined {\n  if (el.lngLat) return el.lngLat\n  if (el.longitude != null && el.latitude != null) return [el.longitude, el.latitude]\n  return undefined\n}\n\n/** Children notify their map when their properties change. */\nfunction notifyMap(el: HTMLElement) {\n  el.closest<UipMap>('uip-map')?.requestSync()\n}\n\n// ── <uip-map> ─────────────────────────────────────────────────────────────────\n\n/**\n * <uip-map> — the registry Map (Mapbox GL) as a web component.\n *\n * Classes, props and behaviour are React's `Map`: `mapVariants` on the wrapper,\n * theme-aware light/dark basemap, muted canvas, 3D buildings/terrain, nav +\n * fullscreen controls, and the \"Mapbox token required\" fallback when there is\n * no `access-token`. mapbox-gl is imported lazily (only with a token, once the\n * element is near the viewport), disposed on disconnect, and resized by a\n * ResizeObserver. Mapbox's CSS and React's map.css are adopted into the shadow\n * root, because Mapbox renders its canvas, controls and popups there.\n *\n * Children (the web-component stand-in for React's JSX children):\n *  - `<uip-map-marker>` / `<uip-map-popup>` — their light-DOM content is slotted\n *    into the Mapbox marker/popup, so it stays page-styled and framework-owned.\n *  - `<uip-map-source>` / `<uip-map-layer>` — declarative GeoJSON/vector data.\n *\n * `center` / `zoom` are the initial view (React's `initialViewState`); `pitch`,\n * `bearing`, style, projection and controls are applied live.\n *\n * Events: `created` (detail: { map }) once the style has loaded — React's\n * `onCreated`. The raw instance is also on the `map` property.\n */\nexport class UipMap extends LitElement {\n  // Manual slot assignment: each marker/popup child is assigned to a <slot>\n  // created inside its Mapbox container; sources/layers are never rendered.\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, slotAssignment: 'manual' as const }\n  // Mapbox's own DOM (canvas, controls, popups, markers) lives in this shadow\n  // root and can only be styled by its vendor CSS + React's map.css overrides.\n  static styles = [tailwind, unsafeCSS(mapboxCss), unsafeCSS(mapCss), css`:host { display: grid; }`]\n\n  static properties = {\n    accessToken: { attribute: 'access-token' },\n    variant: { reflect: true },\n    size: { reflect: true },\n    mapStyle: { attribute: 'map-style' },\n    center: { type: Array },\n    zoom: { type: Number },\n    pitch: { type: Number },\n    bearing: { type: Number },\n    buildings3d: { type: Boolean },\n    terrain3d: { type: Boolean },\n    projection: {},\n    navigation: { converter: boolAttr },\n    navigationPosition: { attribute: 'navigation-position' },\n    showCompass: { attribute: 'show-compass', converter: optionalBool },\n    showZoom: { attribute: 'show-zoom', converter: boolAttr },\n    fullscreen: { type: Boolean },\n    fullscreenPosition: { attribute: 'fullscreen-position' },\n    muted: { type: Boolean },\n    dark: { state: true },\n  }\n\n  /** Mapbox access token. Empty/absent renders the \"token required\" fallback. */\n  accessToken?: string\n  variant: MapVariant = 'default'\n  /** Height preset. Omit to size the host with classes (e.g. `size-full`). */\n  size?: MapVariants['size']\n  /** Custom style URL; overrides `variant` and the theme-aware default. */\n  mapStyle?: string\n  center: LngLat = [0, 20]\n  zoom = 1.4\n  pitch = 0\n  bearing = 0\n  buildings3d = false\n  terrain3d = false\n  projection = 'mercator'\n  navigation = true\n  navigationPosition: Corner = 'bottom-right'\n  /** Defaults to `pitch > 0` when unset. */\n  showCompass?: boolean\n  showZoom = true\n  fullscreen = false\n  fullscreenPosition: Corner = 'top-right'\n  muted = false\n  private dark = false\n\n  private lib?: Mapbox\n  private instance?: mapboxgl.Map\n  private loaded = false\n  private initializing = false\n  private inView = false\n  private io?: IntersectionObserver\n  private ro?: ResizeObserver\n  private themeObserver?: MutationObserver\n  private childObserver?: MutationObserver\n  private navCtrl?: mapboxgl.NavigationControl\n  private fsCtrl?: mapboxgl.FullscreenControl\n  private markers = new Map<UipMapMarker, { marker: mapboxgl.Marker; key: string }>()\n  private popups = new Map<UipMapPopup, { popup: mapboxgl.Popup; key: string }>()\n  private layerSpecs = new Map<Element, { id: string; key: string }>()\n  private sourceSpecs = new Map<Element, { id: string; key: string; data?: unknown }>()\n  private syncQueued = false\n  private appliedStyle?: string\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  /** The raw mapbox-gl Map once created (React's `ref.getMap()`). */\n  get map(): mapboxgl.Map | undefined {\n    return this.instance\n  }\n\n  private get isMuted() {\n    return this.muted || this.variant === 'muted'\n  }\n\n  private get resolvedStyle() {\n    if (this.mapStyle) return this.mapStyle\n    if (this.variant && this.variant !== 'default' && this.variant !== 'muted' && MAPBOX_STYLES[this.variant as keyof typeof MAPBOX_STYLES]) {\n      return MAPBOX_STYLES[this.variant as keyof typeof MAPBOX_STYLES]\n    }\n    return this.dark ? 'mapbox://styles/mapbox/dark-v11' : 'mapbox://styles/mapbox/light-v11'\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'map')\n    const root = document.documentElement\n    this.dark = isDarkPage()\n    this.themeObserver = new MutationObserver(() => (this.dark = isDarkPage()))\n    this.themeObserver.observe(root, { attributes: true, attributeFilter: ['class'] })\n    // Additions/removals of marker/popup/source/layer children.\n    this.childObserver = new MutationObserver(() => this.requestSync())\n    this.childObserver.observe(this, { childList: true, subtree: true })\n    if (this.hasUpdated) this.observeViewport()\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.themeObserver?.disconnect()\n    this.childObserver?.disconnect()\n    this.io?.disconnect()\n    this.ro?.disconnect()\n    this.destroy()\n  }\n\n  willUpdate() {\n    this.setAttribute('data-variant', this.variant)\n    this.setAttribute('data-muted', String(this.isMuted))\n  }\n\n  protected firstUpdated() {\n    this.observeViewport()\n  }\n\n  // Latch like React: the observer only defers the first paint; leaving the\n  // viewport never tears the WebGL map down.\n  private observeViewport() {\n    if (this.inView) return void this.init()\n    if (typeof IntersectionObserver === 'undefined') {\n      this.inView = true\n      return void this.init()\n    }\n    this.io?.disconnect()\n    this.io = new IntersectionObserver(\n      ([entry]) => {\n        if (!entry.isIntersecting) return\n        this.inView = true\n        this.io?.disconnect()\n        this.init()\n      },\n      { rootMargin: '160px', threshold: 0.01 },\n    )\n    this.io.observe(this)\n  }\n\n  private async init() {\n    if (!this.accessToken || !this.inView || this.instance || this.initializing || !this.isConnected) return\n    this.initializing = true\n    try {\n      const lib = (this.lib ??= (await import('mapbox-gl')).default)\n      await this.updateComplete\n      const container = this.renderRoot.querySelector<HTMLElement>('[data-map-container]')\n      if (!container || !this.isConnected || !this.accessToken) return\n      const map = new lib.Map({\n        container,\n        accessToken: this.accessToken,\n        style: (this.appliedStyle = this.resolvedStyle),\n        center: this.center,\n        zoom: this.zoom,\n        pitch: this.pitch,\n        bearing: this.bearing,\n        attributionControl: false,\n      })\n      this.instance = map\n      map.on('load', () => this.onLoad(map))\n      map.on('styledata', () => this.onStyleData(map))\n      // setStyle() drops every source/layer: re-add the declarative ones.\n      map.on('style.load', () => {\n        if (!this.loaded) return\n        this.sourceSpecs.clear()\n        this.layerSpecs.clear()\n        this.syncChildren()\n      })\n      this.ro = new ResizeObserver(() => this.instance?.resize())\n      this.ro.observe(this)\n      this.applyControls()\n    } finally {\n      this.initializing = false\n    }\n  }\n\n  private onLoad(map: mapboxgl.Map) {\n    this.loaded = true\n    // The container may have settled its size after construction.\n    map.resize()\n    try {\n      map.setProjection(this.projection as any)\n    } catch {\n      /* older api */\n    }\n    if (this.pitch) {\n      try {\n        map.setPitch(this.pitch)\n      } catch {\n        /* ignore */\n      }\n    }\n    if (this.bearing) {\n      try {\n        map.setBearing(this.bearing)\n      } catch {\n        /* ignore */\n      }\n    }\n    if (this.isMuted) applyMuted(map, this.dark)\n    if (this.buildings3d) apply3DBuildings(map, this.dark)\n    if (this.terrain3d) apply3DTerrain(map)\n    this.syncChildren()\n    this.dispatchEvent(new CustomEvent('created', { detail: { map }, bubbles: true, composed: true }))\n  }\n\n  private onStyleData(map: mapboxgl.Map) {\n    if (this.isMuted) applyMuted(map, this.dark)\n    if (this.buildings3d) apply3DBuildings(map, this.dark)\n    if (this.terrain3d) apply3DTerrain(map)\n  }\n\n  private destroy() {\n    for (const { marker } of this.markers.values()) marker.remove()\n    for (const { popup } of this.popups.values()) popup.remove()\n    this.markers.clear()\n    this.popups.clear()\n    this.sourceSpecs.clear()\n    this.layerSpecs.clear()\n    this.navCtrl = this.fsCtrl = undefined\n    this.instance?.remove()\n    this.instance = undefined\n    this.loaded = false\n  }\n\n  private applyControls() {\n    const map = this.instance\n    const lib = this.lib\n    if (!map || !lib) return\n    if (this.navCtrl) map.removeControl(this.navCtrl)\n    if (this.fsCtrl) map.removeControl(this.fsCtrl)\n    this.navCtrl = this.fsCtrl = undefined\n    if (this.navigation) {\n      this.navCtrl = new lib.NavigationControl({\n        showCompass: this.showCompass !== undefined ? this.showCompass : (this.pitch ?? 0) > 0,\n        showZoom: this.showZoom,\n      })\n      map.addControl(this.navCtrl, this.navigationPosition)\n    }\n    if (this.fullscreen) {\n      this.fsCtrl = new lib.FullscreenControl()\n      map.addControl(this.fsCtrl, this.fullscreenPosition)\n    }\n  }\n\n  protected updated(changed: PropertyValues) {\n    if (changed.has('accessToken') && this.hasUpdated) {\n      this.destroy()\n      this.init()\n    }\n    const map = this.instance\n    if (!map) return\n    // Only reload when the resolved URL changed (the theme only matters for the default style).\n    const nextStyle = this.resolvedStyle\n    if (nextStyle !== this.appliedStyle) {\n      this.appliedStyle = nextStyle\n      map.setStyle(nextStyle)\n    }\n    if (!this.loaded) return\n    if (changed.has('pitch')) {\n      try {\n        map.setPitch(this.pitch)\n      } catch {\n        /* ignore */\n      }\n    }\n    if (changed.has('bearing')) {\n      try {\n        map.setBearing(this.bearing)\n      } catch {\n        /* ignore */\n      }\n    }\n    if (changed.has('projection')) {\n      try {\n        map.setProjection(this.projection as any)\n      } catch {\n        /* ignore */\n      }\n    }\n    if ((changed.has('muted') || changed.has('dark')) && this.isMuted) applyMuted(map, this.dark)\n    if (changed.has('buildings3d') && this.buildings3d) apply3DBuildings(map, this.dark)\n    if (changed.has('terrain3d') && this.terrain3d) apply3DTerrain(map)\n    if (\n      ['navigation', 'navigationPosition', 'showCompass', 'showZoom', 'fullscreen', 'fullscreenPosition'].some((k) =>\n        changed.has(k),\n      ) ||\n      (changed.has('pitch') && this.showCompass === undefined)\n    ) {\n      this.applyControls()\n    }\n  }\n\n  /** Batched: re-reconcile children with the Mapbox instance. */\n  requestSync() {\n    if (this.syncQueued) return\n    this.syncQueued = true\n    queueMicrotask(() => {\n      this.syncQueued = false\n      this.syncChildren()\n    })\n  }\n\n  /** A <slot> inside a Mapbox-owned container, manually assigned to `el`. */\n  private slotFor(el: HTMLElement) {\n    const holder = document.createElement('div')\n    const slot = document.createElement('slot')\n    holder.append(slot)\n    slot.assign(el)\n    return holder\n  }\n\n  private syncChildren() {\n    const map = this.instance\n    const lib = this.lib\n    if (!map || !lib || !this.loaded) return\n    const kids = [...this.children]\n\n    // Markers.\n    const markerEls = kids.filter((k): k is UipMapMarker => k instanceof UipMapMarker)\n    for (const [el, entry] of this.markers) {\n      if (!markerEls.includes(el)) {\n        entry.marker.remove()\n        this.markers.delete(el)\n      }\n    }\n    for (const el of markerEls) {\n      const lngLat = coordsOf(el)\n      if (!lngLat) continue\n      const hasContent = el.childNodes.length > 0\n      const key = JSON.stringify([el.anchor, hasContent, el.color, el.pitchAlignment, el.rotationAlignment])\n      let entry = this.markers.get(el)\n      if (entry && entry.key !== key) {\n        entry.marker.remove()\n        entry = undefined\n      }\n      if (!entry) {\n        const holder = hasContent ? this.slotFor(el) : undefined\n        const marker = new lib.Marker({\n          element: holder,\n          anchor: el.anchor,\n          color: el.color,\n          pitchAlignment: el.pitchAlignment,\n          rotationAlignment: el.rotationAlignment,\n        })\n        marker.setLngLat(lngLat).addTo(map)\n        entry = { marker, key }\n        this.markers.set(el, entry)\n      }\n      entry.marker.setLngLat(lngLat)\n      entry.marker.setOffset(el.offset ?? [0, 0])\n      entry.marker.setRotation(el.rotation ?? 0)\n    }\n\n    // Popups: present = open (React renders <MapPopup> conditionally).\n    const popupEls = kids.filter((k): k is UipMapPopup => k instanceof UipMapPopup)\n    for (const [el, entry] of this.popups) {\n      if (!popupEls.includes(el)) {\n        entry.popup.off('close', (entry.popup as any).__uipClose)\n        entry.popup.remove()\n        this.popups.delete(el)\n      }\n    }\n    for (const el of popupEls) {\n      const lngLat = coordsOf(el)\n      if (!lngLat) continue\n      const key = JSON.stringify([el.anchor, el.closeButton, el.closeOnClick])\n      let entry = this.popups.get(el)\n      if (entry && entry.key !== key) {\n        entry.popup.off('close', (entry.popup as any).__uipClose)\n        entry.popup.remove()\n        entry = undefined\n      }\n      if (!entry) {\n        const popup = new lib.Popup({\n          anchor: el.anchor,\n          offset: el.offset,\n          closeButton: el.closeButton,\n          closeOnClick: el.closeOnClick,\n          maxWidth: el.maxWidth,\n        })\n        const onClose = () => el.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))\n        ;(popup as any).__uipClose = onClose\n        popup.on('close', onClose)\n        popup.setLngLat(lngLat).setDOMContent(this.slotFor(el)).addTo(map)\n        entry = { popup, key }\n        this.popups.set(el, entry)\n      } else if (entry.popup.isOpen()) {\n        entry.popup.setLngLat(lngLat)\n        if (el.offset !== undefined) entry.popup.setOffset(el.offset)\n        if (el.maxWidth) entry.popup.setMaxWidth(el.maxWidth)\n      }\n    }\n\n    // Sources, then layers (a layer inside a source defaults to that source).\n    const sourceEls = kids.filter((k): k is UipMapSource => k instanceof UipMapSource)\n    const layerEls = [\n      ...sourceEls.flatMap((s) => [...s.children].filter((c): c is UipMapLayer => c instanceof UipMapLayer)),\n      ...kids.filter((k): k is UipMapLayer => k instanceof UipMapLayer),\n    ]\n    for (const [el, spec] of this.layerSpecs) {\n      if (!layerEls.includes(el as UipMapLayer)) {\n        if (map.getLayer(spec.id)) map.removeLayer(spec.id)\n        this.layerSpecs.delete(el)\n      }\n    }\n    for (const [el, spec] of this.sourceSpecs) {\n      if (!sourceEls.includes(el as UipMapSource)) {\n        if (map.getSource(spec.id)) map.removeSource(spec.id)\n        this.sourceSpecs.delete(el)\n      }\n    }\n    for (const el of sourceEls) {\n      if (!el.id) continue\n      const spec = el.spec()\n      const { data, ...rest } = spec as { data?: unknown }\n      const key = JSON.stringify([el.id, rest])\n      const prev = this.sourceSpecs.get(el)\n      if (prev && prev.key === key) {\n        if (prev.data !== data && data !== undefined) {\n          ;(map.getSource(el.id) as mapboxgl.GeoJSONSource | undefined)?.setData?.(data as any)\n          prev.data = data\n        }\n        continue\n      }\n      if (prev) {\n        // Spec changed: drop its layers first, they're re-added below.\n        for (const [lel, lspec] of this.layerSpecs) {\n          if (map.getLayer(lspec.id) && (map.getLayer(lspec.id) as any).source === prev.id) {\n            map.removeLayer(lspec.id)\n            this.layerSpecs.delete(lel)\n          }\n        }\n        if (map.getSource(prev.id)) map.removeSource(prev.id)\n      }\n      if (!map.getSource(el.id)) map.addSource(el.id, spec as any)\n      this.sourceSpecs.set(el, { id: el.id, key, data })\n    }\n    for (const el of layerEls) {\n      if (!el.id) continue\n      const parent = el.parentElement instanceof UipMapSource ? el.parentElement.id : undefined\n      const spec = el.spec(parent)\n      const key = JSON.stringify([spec, el.beforeId])\n      const prev = this.layerSpecs.get(el)\n      if (prev && prev.key === key && map.getLayer(prev.id)) continue\n      if (prev && map.getLayer(prev.id)) map.removeLayer(prev.id)\n      if (spec.source && typeof spec.source === 'string' && !map.getSource(spec.source)) continue\n      map.addLayer(spec as any, el.beforeId && map.getLayer(el.beforeId) ? el.beforeId : undefined)\n      this.layerSpecs.set(el, { id: el.id, key })\n    }\n  }\n\n  render() {\n    const token = this.accessToken\n    return html`<div\n      part=\"base\"\n      data-variant=${this.variant}\n      data-muted=${String(this.isMuted)}\n      class=${cn(mapVariants({ variant: this.variant, ...(this.size ? { size: this.size } : {}) }), 'rounded-[inherit]')}\n    >\n      ${token\n        ? html`<div data-map-container class=\"relative size-full\"></div>`\n        : html`<div class=\"text-muted-foreground flex size-full flex-col items-center justify-center gap-1 px-6 text-center text-sm\">\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    </div>`\n  }\n}\n\n// ── <uip-map-marker> ──────────────────────────────────────────────────────────\n\n/**\n * <uip-map-marker> — React's `MapMarker`. Place inside `<uip-map>`; its content\n * is the marker (no content = Mapbox's default pin, tinted by `color`).\n * Position with `lng-lat=\"[lng, lat]\"` or `longitude` + `latitude`.\n * Clicks bubble from the element itself (React's `onClick`).\n */\nexport class UipMapMarker extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    lngLat: { attribute: 'lng-lat', type: Array },\n    longitude: { type: Number },\n    latitude: { type: Number },\n    anchor: {},\n    offset: { converter: numberOrJson },\n    rotation: { type: Number },\n    color: {},\n    pitchAlignment: { attribute: 'pitch-alignment' },\n    rotationAlignment: { attribute: 'rotation-alignment' },\n  }\n\n  lngLat?: LngLat\n  longitude?: number\n  latitude?: number\n  anchor?: mapboxgl.Anchor\n  offset?: [number, number]\n  rotation?: number\n  color?: string\n  pitchAlignment?: 'map' | 'viewport' | 'auto'\n  rotationAlignment?: 'map' | 'viewport' | 'auto' | 'horizon'\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-slot', 'map-marker')\n  }\n\n  protected updated() {\n    notifyMap(this)\n  }\n\n  render() {\n    return html`<slot></slot>`\n  }\n}\n\n// ── <uip-map-popup> ───────────────────────────────────────────────────────────\n\n/**\n * <uip-map-popup> — React's `MapPopup`. Present = open (React renders it\n * conditionally); remove the element to close it.\n * Events: `close` when the user closes it (close button / map click) — React's `onClose`.\n */\nexport class UipMapPopup extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    lngLat: { attribute: 'lng-lat', type: Array },\n    longitude: { type: Number },\n    latitude: { type: Number },\n    anchor: {},\n    offset: { converter: numberOrJson },\n    closeButton: { attribute: 'close-button', converter: boolAttr },\n    closeOnClick: { attribute: 'close-on-click', converter: boolAttr },\n    maxWidth: { attribute: 'max-width' },\n  }\n\n  lngLat?: LngLat\n  longitude?: number\n  latitude?: number\n  anchor?: mapboxgl.Anchor\n  offset?: number | [number, number]\n  closeButton = true\n  closeOnClick = true\n  maxWidth?: string\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-slot', 'map-popup')\n  }\n\n  protected updated() {\n    notifyMap(this)\n  }\n\n  render() {\n    return html`<slot></slot>`\n  }\n}\n\n// ── <uip-map-source> / <uip-map-layer> ────────────────────────────────────────\n\n/**\n * <uip-map-source> — React's `MapSource`. The element's `id` is the source id.\n * Spec: `type` + `data` (property or JSON attribute), or a full `options` object\n * (React's Vue-parity `options` prop). Changing `data` on a GeoJSON source calls\n * `setData`. Put `<uip-map-layer>`s inside to draw it. Never rendered.\n */\nexport class UipMapSource extends LitElement {\n  static styles = [css`:host { display: none; }`]\n\n  static properties = {\n    type: {},\n    data: { type: Object },\n    url: {},\n    options: { type: Object },\n  }\n\n  type?: string\n  data?: unknown\n  url?: string\n  options?: Record<string, unknown>\n\n  spec(): Record<string, unknown> {\n    const s: Record<string, unknown> = { ...(this.options ?? {}) }\n    if (this.type) s.type = this.type\n    if (this.data !== undefined) s.data = this.data\n    if (this.url) s.url = this.url\n    return s\n  }\n\n  protected updated() {\n    notifyMap(this)\n  }\n\n  render() {\n    return html``\n  }\n}\n\n/**\n * <uip-map-layer> — React's `MapLayer`. The element's `id` is the layer id;\n * `source` defaults to the wrapping `<uip-map-source>`. Spec from `type`,\n * `layout`, `paint`, `filter`, `source-layer`, `minzoom`, `maxzoom`, or a full\n * `options` object. `before-id` inserts it under an existing layer. Never rendered.\n */\nexport class UipMapLayer extends LitElement {\n  static styles = [css`:host { display: none; }`]\n\n  static properties = {\n    type: {},\n    source: {},\n    sourceLayer: { attribute: 'source-layer' },\n    layout: { type: Object },\n    paint: { type: Object },\n    filter: { type: Array },\n    minzoom: { type: Number },\n    maxzoom: { type: Number },\n    beforeId: { attribute: 'before-id' },\n    options: { type: Object },\n  }\n\n  type?: string\n  source?: string\n  sourceLayer?: string\n  layout?: Record<string, unknown>\n  paint?: Record<string, unknown>\n  filter?: unknown[]\n  minzoom?: number\n  maxzoom?: number\n  beforeId?: string\n  options?: Record<string, unknown>\n\n  spec(parentSource?: string): Record<string, unknown> {\n    const s: Record<string, unknown> = { ...(this.options ?? {}), id: this.id }\n    const source = this.source ?? parentSource\n    if (source) s.source = source\n    if (this.type) s.type = this.type\n    if (this.sourceLayer) s['source-layer'] = this.sourceLayer\n    if (this.layout) s.layout = this.layout\n    if (this.paint) s.paint = this.paint\n    if (this.filter) s.filter = this.filter\n    if (this.minzoom !== undefined) s.minzoom = this.minzoom\n    if (this.maxzoom !== undefined) s.maxzoom = this.maxzoom\n    return s\n  }\n\n  protected updated() {\n    notifyMap(this)\n  }\n\n  render() {\n    return html``\n  }\n}\n\ncustomElements.get('uip-map') || customElements.define('uip-map', UipMap)\ncustomElements.get('uip-map-marker') || customElements.define('uip-map-marker', UipMapMarker)\ncustomElements.get('uip-map-popup') || customElements.define('uip-map-popup', UipMapPopup)\ncustomElements.get('uip-map-source') || customElements.define('uip-map-source', UipMapSource)\ncustomElements.get('uip-map-layer') || customElements.define('uip-map-layer', UipMapLayer)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-map': UipMap\n    'uip-map-marker': UipMapMarker\n    'uip-map-popup': UipMapPopup\n    'uip-map-source': UipMapSource\n    'uip-map-layer': UipMapLayer\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/map/map.ts"
    },
    {
      "path": "packages/elements-lit/src/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:file",
      "target": "src/uip/components/map/map.variants.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit",
    "mapbox-gl"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
