{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "map",
  "title": "Map",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/map/Map.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n  // Module-script imports are visible to the instance script; importing the\n  // value here (instead of `import type` + a second instance import) avoids a\n  // duplicate-identifier error — both scripts share one scope.\n  import mapboxgl from 'mapbox-gl'\n  import type { MapVariant, MapVariants } from './map.variants'\n\n  export type MapControlPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n\n  export interface MapProps extends HTMLAttributes<HTMLDivElement> {\n    /** Mapbox access token. */\n    accessToken?: string\n    /** Named Mapbox basemap variant preset ('streets' | 'outdoors' | 'satellite' | 'satellite-streets' | 'light' | 'dark' | 'navigation-day' | 'navigation-night' | 'standard' | 'muted' | 'default'). */\n    variant?: MapVariant\n    /** Height preset. Omit to size via `class` (blocks typically pass `size-full`). */\n    size?: MapVariants['size']\n    /** Override the base style. Defaults to variant or theme-aware light/dark style. */\n    mapStyle?: string\n    /** Initial [lng, lat]. */\n    center?: [number, number]\n    zoom?: number\n    /** Camera pitch tilt angle in degrees (0 - 85). */\n    pitch?: number\n    /** Camera bearing rotation angle in degrees (0 - 360). */\n    bearing?: number\n    /** Automatically add 3D extruded building footprints. */\n    buildings3d?: boolean\n    /** Enable 3D digital elevation model terrain mesh. */\n    terrain3d?: boolean\n    /** 'mercator' (flat) or 'globe'. */\n    projection?: string\n    /** Show the zoom/compass control. */\n    navigation?: boolean\n    /** Placement position of the navigation control. */\n    navigationPosition?: MapControlPosition\n    /** Explicitly show/hide compass in navigation control. Defaults to true when pitch > 0. */\n    showCompass?: boolean\n    /** Explicitly show/hide zoom buttons in navigation control. Defaults to true. */\n    showZoom?: boolean\n    /** Show the HTML5 fullscreen toggle control button. */\n    fullscreen?: boolean\n    /** Placement position of the fullscreen control. Defaults to 'top-right'. */\n    fullscreenPosition?: MapControlPosition\n    /** Repaint the basemap to a quiet, desaturated canvas. */\n    muted?: boolean\n    /** Fires with the raw mapbox-gl `Map` once the style is ready. */\n    oncreated?: (map: mapboxgl.Map) => void\n    children?: Snippet\n    ref?: HTMLDivElement | null\n  }\n\n  /** Handle returned by `bind:this` on <Map> — mirrors react-map-gl's MapRef.\n   *  Carries the camera helpers plus `getMap()` for the raw mapbox-gl Map. */\n  export interface MapRef {\n    getMap: () => mapboxgl.Map | null\n    flyTo: (...args: Parameters<mapboxgl.Map['flyTo']>) => void\n    easeTo: (...args: Parameters<mapboxgl.Map['easeTo']>) => void\n    jumpTo: (...args: Parameters<mapboxgl.Map['jumpTo']>) => void\n    fitBounds: (...args: Parameters<mapboxgl.Map['fitBounds']>) => void\n    resize: () => void\n  }\n</script>\n\n<script lang=\"ts\">\n  /**\n   * Map — a thin, theme-aware Mapbox GL JS wrapper. Pass an `accessToken`;\n   * drop `<MapMarker>` / `<MapPopup>` / `<MapSource>` / `<MapLayer>` into the\n   * children to build any kind of map.\n   *\n   * Requires a Mapbox access token (https://account.mapbox.com).\n   */\n  import { onDestroy, onMount } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { setMapContext } from './map-context.svelte'\n  import { mapVariants, MAPBOX_STYLES } from './map.variants'\n  import 'mapbox-gl/dist/mapbox-gl.css'\n  import './map.css'\n\n  const DEFAULT_MAPBOX_TOKEN = ''\n\n  function envToken(): string {\n    return (\n      (typeof process !== 'undefined' &&\n        (process.env?.NEXT_PUBLIC_MAPBOX_TOKEN || process.env?.PUBLIC_MAPBOX_TOKEN)) ||\n      (typeof import.meta !== 'undefined' && (import.meta as any).env?.PUBLIC_MAPBOX_TOKEN) ||\n      DEFAULT_MAPBOX_TOKEN\n    )\n  }\n\n  let {\n    accessToken = envToken(),\n    variant = 'default',\n    size,\n    mapStyle,\n    center = [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 = 'bottom-right',\n    showCompass,\n    showZoom = true,\n    fullscreen = false,\n    fullscreenPosition = 'top-right',\n    muted = false,\n    oncreated,\n    children,\n    ref = $bindable(null),\n    class: className,\n    ...restProps\n  }: MapProps = $props()\n\n  const resolvedToken = $derived.by(() => {\n    // Explicit empty string opts out of the demo fallback — used by \"missing token\" stories.\n    if (accessToken === '') return ''\n    return accessToken || envToken()\n  })\n\n  let htmlDark = $state(false)\n  const isDark = () => htmlDark\n\n  const isMuted = $derived(muted || variant === 'muted')\n\n  const resolvedStyle = $derived.by(() => {\n    if (mapStyle) return mapStyle\n    if (\n      variant &&\n      variant !== 'default' &&\n      variant !== 'muted' &&\n      MAPBOX_STYLES[variant as keyof typeof MAPBOX_STYLES]\n    ) {\n      return MAPBOX_STYLES[variant as keyof typeof MAPBOX_STYLES]\n    }\n    return htmlDark ? 'mapbox://styles/mapbox/dark-v11' : 'mapbox://styles/mapbox/light-v11'\n  })\n\n  // Render the map only on the client — Mapbox needs the DOM, and rendering it\n  // on the server would hydrate-mismatch. The bg-muted box is the SSR placeholder.\n  let mounted = $state(false)\n  let inView = $state(false)\n  let mapEl = $state<HTMLDivElement | null>(null)\n  let mapInstance = $state<mapboxgl.Map | null>(null)\n  let lastStyle: string | null = null\n  let resizeObserver: ResizeObserver | null = null\n  let intersectionObserver: IntersectionObserver | null = null\n  let themeObserver: MutationObserver | null = null\n\n  // Published to MapMarker / MapPopup / MapSource / MapLayer children.\n  setMapContext({\n    get map() {\n      return mapInstance\n    },\n  })\n\n  onMount(() => {\n    const root = document.documentElement\n    const syncTheme = () => {\n      htmlDark = root.classList.contains('dark')\n    }\n    syncTheme()\n    themeObserver = new MutationObserver(syncTheme)\n    themeObserver.observe(root, { attributes: true, attributeFilter: ['class'] })\n    mounted = true\n    if (!ref) {\n      inView = true\n      return\n    }\n    if (typeof IntersectionObserver !== 'undefined') {\n      // Latch: the observer only defers the first paint. Un-setting it on scroll\n      // would tear the WebGL map down and rebuild it (fresh style fetch, camera\n      // reset, markers re-laid-out against a stale canvas size) every time the\n      // block leaves the viewport.\n      intersectionObserver = new IntersectionObserver(\n        ([entry]) => {\n          if (!entry!.isIntersecting) return\n          inView = true\n          intersectionObserver?.disconnect()\n          intersectionObserver = null\n        },\n        { rootMargin: '160px', threshold: 0.01 },\n      )\n      intersectionObserver.observe(ref)\n    } else {\n      inView = true\n    }\n    if (typeof ResizeObserver !== 'undefined' && ref) {\n      const container = ref\n      resizeObserver = new ResizeObserver(() => {\n        mapInstance?.resize()\n      })\n      resizeObserver.observe(container)\n    }\n  })\n\n  onDestroy(() => {\n    resizeObserver?.disconnect()\n    resizeObserver = null\n    intersectionObserver?.disconnect()\n    intersectionObserver = null\n    themeObserver?.disconnect()\n    themeObserver = null\n    try {\n      mapInstance?.remove()\n    } catch {\n      /* already destroyed */\n    }\n    mapInstance = null\n  })\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. */\n  function applyMuted() {\n    const map = mapInstance\n    if (!map) return\n    let styleLayers: any[]\n    try {\n      styleLayers = map.getStyle()?.layers ?? []\n    } catch {\n      return // style not loaded yet\n    }\n    const dark = isDark()\n    const P = 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  function apply3DBuildings() {\n    const map = mapInstance\n    if (!map || !buildings3d) return\n    if (map.getLayer('3d-buildings')) return\n\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\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': isDark() ? '#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  function apply3DTerrain() {\n    const map = mapInstance\n    if (!map || !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  let navControl: mapboxgl.NavigationControl | null = null\n  let fullscreenControl: mapboxgl.FullscreenControl | null = null\n\n  function updateNavControl() {\n    const map = mapInstance\n    if (!map) return\n    if (navControl) {\n      try {\n        map.removeControl(navControl)\n      } catch {\n        /* ignore */\n      }\n      navControl = null\n    }\n    if (navigation) {\n      const compass = showCompass !== undefined ? showCompass : pitch > 0\n      navControl = new mapboxgl.NavigationControl({ showCompass: compass, showZoom })\n      map.addControl(navControl, navigationPosition ?? 'bottom-right')\n    }\n  }\n\n  function updateFullscreenControl() {\n    const map = mapInstance\n    if (!map) return\n    if (fullscreenControl) {\n      try {\n        map.removeControl(fullscreenControl)\n      } catch {\n        /* ignore */\n      }\n      fullscreenControl = null\n    }\n    if (fullscreen) {\n      fullscreenControl = new mapboxgl.FullscreenControl()\n      map.addControl(fullscreenControl, fullscreenPosition ?? 'top-right')\n    }\n  }\n\n  function createMap() {\n    const el = mapEl\n    const token = resolvedToken\n    if (!el || !token || mapInstance) return\n    const map = new mapboxgl.Map({\n      container: el,\n      accessToken: token,\n      style: resolvedStyle,\n      center,\n      zoom,\n      attributionControl: false,\n    })\n    mapInstance = map\n    lastStyle = resolvedStyle\n    // The ResizeObserver's first callback runs before this instance exists, so a\n    // container that settled its height during mount would leave the canvas --\n    // and every marker laid out against it -- at the stale size.\n    map.resize()\n    try {\n      ;(map as any).setProjection(projection)\n    } catch {\n      /* older api */\n    }\n    if (pitch) {\n      try {\n        map.setPitch(pitch)\n      } catch {\n        /* ignore */\n      }\n    }\n    if (bearing) {\n      try {\n        map.setBearing(bearing)\n      } catch {\n        /* ignore */\n      }\n    }\n\n    updateNavControl()\n    updateFullscreenControl()\n\n    const ready = () => {\n      if (isMuted) applyMuted()\n      if (buildings3d) apply3DBuildings()\n      if (terrain3d) apply3DTerrain()\n      oncreated?.(map)\n    }\n    if (map.isStyleLoaded()) ready()\n    else map.once('load', ready)\n    map.on('style.load', () => {\n      if (isMuted) applyMuted()\n      if (buildings3d) apply3DBuildings()\n      if (terrain3d) apply3DTerrain()\n    })\n  }\n\n  $effect(() => {\n    if (mounted && inView && resolvedToken && mapEl && !mapInstance) createMap()\n  })\n\n  // Re-style on variant / theme / mapStyle changes (skip the initial run —\n  // createMap already applied it).\n  $effect(() => {\n    const style = resolvedStyle\n    if (mapInstance && style !== lastStyle) {\n      lastStyle = style\n      try {\n        mapInstance.setStyle(style)\n      } catch {\n        /* style still configuring */\n      }\n    }\n  })\n\n  $effect(() => {\n    if (mapInstance && pitch !== undefined) mapInstance.setPitch(pitch)\n  })\n\n  $effect(() => {\n    if (mapInstance && bearing !== undefined) mapInstance.setBearing(bearing)\n  })\n\n  $effect(() => {\n    void [navigation, navigationPosition, showCompass, showZoom]\n    updateNavControl()\n  })\n\n  $effect(() => {\n    void [fullscreen, fullscreenPosition]\n    updateFullscreenControl()\n  })\n\n  // `bind:this` on <Map> hands back the mapbox-gl Map itself, plus the camera\n  // helpers blocks reach for directly off the ref.\n  /** The underlying mapbox-gl Map, or null until it is created. */\n  export function getMap() {\n    return mapInstance\n  }\n  // Option types are read off the Map class so they track mapbox-gl's own\n  // signatures instead of drifting against renamed exports.\n  export function flyTo(...args: Parameters<mapboxgl.Map['flyTo']>) {\n    mapInstance?.flyTo(...args)\n  }\n  export function easeTo(...args: Parameters<mapboxgl.Map['easeTo']>) {\n    mapInstance?.easeTo(...args)\n  }\n  export function jumpTo(...args: Parameters<mapboxgl.Map['jumpTo']>) {\n    mapInstance?.jumpTo(...args)\n  }\n  export function fitBounds(...args: Parameters<mapboxgl.Map['fitBounds']>) {\n    mapInstance?.fitBounds(...args)\n  }\n  export function resize() {\n    mapInstance?.resize()\n  }\n</script>\n\n<div\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"map\"\n  data-variant={variant}\n  data-muted={isMuted}\n  class={cn(mapVariants({ variant, ...(size ? { size } : {}) }), className)}\n  {...restProps}\n>\n  {#if mounted && inView && resolvedToken}\n    <div bind:this={mapEl} class=\"size-full\"></div>\n    {@render children?.()}\n  {:else if mounted && !resolvedToken}\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  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/map/Map.svelte"
    },
    {
      "path": "packages/registry-svelte/components/map/MapSource.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { AnySourceData, GeoJSONSource } from 'mapbox-gl'\n\n  export interface MapSourceProps {\n    id: string\n    type?: string\n    data?: any\n    options?: AnySourceData\n    children?: Snippet\n  }\n</script>\n\n<script lang=\"ts\">\n  import { setMapSourceId, useMap } from './map-context.svelte'\n\n  let { id, type, data, options, children }: MapSourceProps = $props()\n\n  const mapCtx = useMap()\n  // svelte-ignore state_referenced_locally — the source id is intentionally initial-only (matches Vue's provide).\n  setMapSourceId(id)\n\n  function getSourceOptions(): AnySourceData {\n    if (options) return options\n    return {\n      type: (type || 'geojson') as any,\n      data,\n    }\n  }\n\n  function addOrUpdateSource(mapInstance: NonNullable<typeof mapCtx.map>) {\n    try {\n      if (mapInstance.getSource(id)) {\n        const src = mapInstance.getSource(id) as GeoJSONSource | undefined\n        if (src && typeof src.setData === 'function' && data) {\n          src.setData(data)\n        }\n        return\n      }\n      mapInstance.addSource(id, getSourceOptions())\n    } catch {\n      // Retry on next frame if style is still configuring\n    }\n  }\n\n  function removeSource(mapInstance: NonNullable<typeof mapCtx.map>) {\n    try {\n      if (mapInstance.getSource(id)) {\n        const layers = mapInstance.getStyle()?.layers || []\n        for (const l of layers) {\n          if ('source' in l && l.source === id) {\n            mapInstance.removeLayer(l.id)\n          }\n        }\n        mapInstance.removeSource(id)\n      }\n    } catch {\n      // Ignored during map destruction\n    }\n  }\n\n  $effect(() => {\n    const mapInstance = mapCtx.map\n    if (!mapInstance) return\n    if (mapInstance.isStyleLoaded()) {\n      addOrUpdateSource(mapInstance)\n    } else {\n      mapInstance.once('style.load', () => addOrUpdateSource(mapInstance))\n      mapInstance.once('load', () => addOrUpdateSource(mapInstance))\n    }\n    return () => removeSource(mapInstance)\n  })\n\n  $effect(() => {\n    const newData = data\n    const mapInstance = mapCtx.map\n    if (!mapInstance || !newData) return\n    try {\n      const src = mapInstance.getSource(id) as GeoJSONSource | undefined\n      if (src && typeof src.setData === 'function') {\n        src.setData(newData)\n      }\n    } catch {\n      // Ignored if source not yet ready or re-rendering\n    }\n  })\n</script>\n\n<div {id} style=\"display: contents\">{@render children?.()}</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/map/MapSource.svelte"
    },
    {
      "path": "packages/registry-svelte/components/map/MapLayer.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { AnyLayer } from 'mapbox-gl'\n\n  export interface MapLayerProps {\n    id: string\n    /** Optional: `options` carries its own `type`, so only the flat form needs it. */\n    type?: string\n    source?: string\n    paint?: Record<string, any>\n    layout?: Record<string, any>\n    beforeId?: string\n    options?: any\n  }\n</script>\n\n<script lang=\"ts\">\n  import { useMap, useMapSourceId } from './map-context.svelte'\n\n  let { id, type, source, paint, layout, beforeId, options }: MapLayerProps = $props()\n\n  const parentSourceId = useMapSourceId()\n  const mapCtx = useMap()\n\n  function getLayerConfig(): AnyLayer {\n    if (options) return { ...options, id }\n    return {\n      id,\n      type: type as any,\n      source: source || parentSourceId,\n      paint: paint || {},\n      layout: layout || {},\n    } as AnyLayer\n  }\n\n  function addLayerSafe(mapInstance: NonNullable<typeof mapCtx.map>) {\n    try {\n      if (mapInstance.getLayer(id)) {\n        mapInstance.removeLayer(id)\n      }\n      const config = getLayerConfig()\n      if (config.source && !mapInstance.getSource(config.source as string)) {\n        // Source not ready yet, wait for sourcedata event\n        const checkAndAdd = () => {\n          try {\n            if (!mapInstance.getLayer(id) && mapInstance.getSource(config.source as string)) {\n              mapInstance.addLayer(config, beforeId)\n              mapInstance.off('sourcedata', checkAndAdd)\n            }\n          } catch {\n            // Ignored\n          }\n        }\n        mapInstance.on('sourcedata', checkAndAdd)\n        return\n      }\n      mapInstance.addLayer(config, beforeId)\n    } catch {\n      // Retry if style loading\n    }\n  }\n\n  $effect(() => {\n    const mapInstance = mapCtx.map\n    if (!mapInstance) return\n    if (mapInstance.isStyleLoaded()) {\n      addLayerSafe(mapInstance)\n    } else {\n      mapInstance.once('style.load', () => addLayerSafe(mapInstance))\n      mapInstance.once('load', () => addLayerSafe(mapInstance))\n    }\n    return () => {\n      try {\n        if (mapInstance.getLayer(id)) {\n          mapInstance.removeLayer(id)\n        }\n      } catch {\n        // Ignored\n      }\n    }\n  })\n\n  $effect(() => {\n    const newPaint = paint\n    const mapInstance = mapCtx.map\n    if (!mapInstance || !newPaint) return\n    try {\n      if (mapInstance.getLayer(id)) {\n        for (const [key, value] of Object.entries(newPaint)) {\n          // Keys come from a caller-supplied record; mapbox generics the name\n          // against a closed union of every paint property, which a plain\n          // string cannot satisfy.\n          ;(mapInstance.setPaintProperty as (layer: string, name: string, value: unknown) => void)(id, key, value)\n        }\n      }\n    } catch {\n      // Ignored\n    }\n  })\n</script>\n\n<div {id} style=\"display: none\"></div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/map/MapLayer.svelte"
    },
    {
      "path": "packages/registry-svelte/components/map/MapMarker.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  // Value import in the module script (visible to the instance script) — both\n  // scripts share one scope, so a second instance import would collide.\n  import mapboxgl from 'mapbox-gl'\n\n  export interface MapMarkerProps {\n    /** [lng, lat] position. */\n    lngLat: [number, number]\n    /** Default-pin colour (only when no `children` custom content is given). */\n    color?: string\n    /** Scale factor for the default pin. */\n    scale?: number\n    draggable?: boolean\n    rotation?: number\n    rotationAlignment?: 'map' | 'viewport' | 'auto'\n    pitchAlignment?: 'map' | 'viewport' | 'auto'\n    /** Which part of the marker sits on the coordinates. Passed to Mapbox. */\n    anchor?:\n      | 'center'\n      | 'top'\n      | 'bottom'\n      | 'left'\n      | 'right'\n      | 'top-left'\n      | 'top-right'\n      | 'bottom-left'\n      | 'bottom-right'\n    /** Class applied to the custom-content wrapper (only rendered with `children`). */\n    class?: string\n    /** Custom marker content. Omit for the default Mapbox pin. */\n    children?: Snippet\n    /** Fires after the marker is dragged. */\n    ondragend?: (ev: { target: mapboxgl.Marker }) => void\n    onclick?: (ev: MouseEvent) => void\n  }\n</script>\n\n<script lang=\"ts\">\n  import { untrack } from 'svelte'\n  import { useMap } from './map-context.svelte'\n\n  let {\n    lngLat,\n    color,\n    scale,\n    draggable,\n    rotation,\n    rotationAlignment,\n    pitchAlignment,\n    anchor,\n    class: className,\n    children,\n    ondragend,\n    onclick,\n  }: MapMarkerProps = $props()\n\n  const mapCtx = useMap()\n  let el = $state<HTMLElement | null>(null)\n  let marker: mapboxgl.Marker | null = null\n\n  $effect(() => {\n    const mapInstance = mapCtx.map\n    const node = el\n    // `children` decides default pin vs custom element; read it so the marker\n    // rebuilds if custom content appears after mount. Everything else is\n    // initial-only — position syncs via the effect below, so option/callback\n    // identity churn must not recreate the marker.\n    const hasCustomContent = Boolean(children)\n    if (!mapInstance || (hasCustomContent && !node)) return\n    const initial = untrack(() => ({\n      lngLat,\n      color,\n      scale,\n      draggable,\n      rotation,\n      rotationAlignment,\n      pitchAlignment,\n      anchor,\n      ondragend,\n      onclick,\n    }))\n    marker = new mapboxgl.Marker({\n      ...(hasCustomContent && node ? { element: node } : {}),\n      ...(initial.color ? { color: initial.color } : {}),\n      ...(initial.scale !== undefined ? { scale: initial.scale } : {}),\n      ...(initial.draggable !== undefined ? { draggable: initial.draggable } : {}),\n      ...(initial.rotation !== undefined ? { rotation: initial.rotation } : {}),\n      ...(initial.rotationAlignment ? { rotationAlignment: initial.rotationAlignment } : {}),\n      ...(initial.pitchAlignment ? { pitchAlignment: initial.pitchAlignment } : {}),\n      ...(initial.anchor ? { anchor: initial.anchor } : {}),\n    })\n      .setLngLat(initial.lngLat)\n      .addTo(mapInstance)\n    if (initial.ondragend) {\n      const cb = initial.ondragend\n      marker.on('dragend', () => cb({ target: marker! }))\n    }\n    const markerNode = marker.getElement()\n    const handleClick = (ev: MouseEvent) => initial.onclick?.(ev)\n    if (initial.onclick) markerNode.addEventListener('click', handleClick)\n    const instance = marker\n    return () => {\n      if (onclick) markerNode.removeEventListener('click', handleClick)\n      instance.remove()\n      if (marker === instance) marker = null\n    }\n  })\n\n  $effect(() => {\n    if (marker && lngLat) marker.setLngLat(lngLat)\n  })\n</script>\n\n{#if children}\n  <!-- Plain wrapper (no display:contents): Mapbox positions this element with\n       a transform, which needs a real box — same box the `class` prop styles. -->\n  <div bind:this={el} class={className}>{@render children()}</div>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/map/MapMarker.svelte"
    },
    {
      "path": "packages/registry-svelte/components/map/MapPopup.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n\n  export interface MapPopupProps {\n    /** [lng, lat] anchor. */\n    lngLat: [number, number]\n    closeButton?: boolean\n    closeOnClick?: boolean\n    closeOnMove?: boolean\n    maxWidth?: string\n    className?: string\n    offset?: number | [number, number]\n    children?: Snippet\n    onopen?: () => void\n    onclose?: () => void\n  }\n</script>\n\n<script lang=\"ts\">\n  import { untrack } from 'svelte'\n  import mapboxgl from 'mapbox-gl'\n  import { useMap } from './map-context.svelte'\n\n  let {\n    lngLat,\n    closeButton,\n    closeOnClick,\n    closeOnMove,\n    maxWidth,\n    className,\n    offset,\n    children,\n    onopen,\n    onclose,\n  }: MapPopupProps = $props()\n\n  const mapCtx = useMap()\n  let el = $state<HTMLElement | null>(null)\n  let popup: mapboxgl.Popup | null = null\n\n  $effect(() => {\n    const mapInstance = mapCtx.map\n    const node = el\n    if (!mapInstance || !node) return\n    // Options are initial-only; position syncs via the effect below.\n    const initial = untrack(() => ({\n      lngLat,\n      closeButton,\n      closeOnClick,\n      closeOnMove,\n      maxWidth,\n      className,\n      offset,\n      onopen,\n      onclose,\n    }))\n    popup = new mapboxgl.Popup({\n      ...(initial.closeButton !== undefined ? { closeButton: initial.closeButton } : {}),\n      ...(initial.closeOnClick !== undefined ? { closeOnClick: initial.closeOnClick } : {}),\n      ...(initial.closeOnMove !== undefined ? { closeOnMove: initial.closeOnMove } : {}),\n      ...(initial.maxWidth ? { maxWidth: initial.maxWidth } : {}),\n      ...(initial.className ? { className: initial.className } : {}),\n      ...(initial.offset !== undefined ? { offset: initial.offset } : {}),\n    })\n      .setLngLat(initial.lngLat)\n      .setDOMContent(node)\n      .addTo(mapInstance)\n    if (initial.onopen) popup.on('open', initial.onopen)\n    if (initial.onclose) popup.on('close', initial.onclose)\n    const instance = popup\n    return () => {\n      instance.remove()\n      if (popup === instance) popup = null\n    }\n  })\n\n  $effect(() => {\n    if (popup && lngLat) popup.setLngLat(lngLat)\n  })\n</script>\n\n<div bind:this={el}>{@render children?.()}</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/map/MapPopup.svelte"
    },
    {
      "path": "packages/registry-svelte/components/map/MapNavigationControl.svelte",
      "content": "<script lang=\"ts\" module>\n  // Value import in the module script (visible to the instance script) — both\n  // scripts share one scope, so a second instance import would collide.\n  import mapboxgl from 'mapbox-gl'\n  import type { MapControlPosition } from './Map.svelte'\n\n  export interface MapNavigationControlProps {\n    /** Control placement. Defaults to 'top-right' (react-map-gl parity). */\n    position?: MapControlPosition\n    /** Show the compass button. Defaults to true. */\n    showCompass?: boolean\n    /** Show the zoom buttons. Defaults to true. */\n    showZoom?: boolean\n    /** Tilt the compass to reflect the camera pitch. Defaults to false. */\n    visualizePitch?: boolean\n  }\n</script>\n\n<script lang=\"ts\">\n  /**\n   * Standalone zoom/compass control — the Svelte equivalent of React's\n   * `MapNavigationControl` (react-map-gl re-export). Must be rendered inside\n   * `<Map>` children; reads the map instance from context. For the common\n   * case prefer `<Map navigation navigationPosition={...}>` — combine a\n   * standalone control with `navigation={false}` to avoid duplicates.\n   */\n  import { useMap } from './map-context.svelte'\n\n  let {\n    position = 'top-right',\n    showCompass = true,\n    showZoom = true,\n    visualizePitch = false,\n  }: MapNavigationControlProps = $props()\n\n  const mapCtx = useMap()\n  let control: mapboxgl.NavigationControl | null = null\n\n  function teardown(map: mapboxgl.Map | null) {\n    if (map && control) {\n      try {\n        map.removeControl(control)\n      } catch {\n        /* already removed */\n      }\n    }\n    control = null\n  }\n\n  $effect(() => {\n    const map = mapCtx.map\n    // Mapbox controls are immutable — recreate when options change.\n    void [position, showCompass, showZoom, visualizePitch]\n    if (!map) return\n    teardown(map)\n    control = new mapboxgl.NavigationControl({ showCompass, showZoom, visualizePitch })\n    map.addControl(control, position)\n    return () => teardown(map)\n  })\n</script>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/map/MapNavigationControl.svelte"
    },
    {
      "path": "packages/registry-svelte/components/map/MapFullscreenControl.svelte",
      "content": "<script lang=\"ts\" module>\n  // Value import in the module script (visible to the instance script) — both\n  // scripts share one scope, so a second instance import would collide.\n  import mapboxgl from 'mapbox-gl'\n  import type { MapControlPosition } from './Map.svelte'\n\n  export interface MapFullscreenControlProps {\n    /** Control placement. Defaults to 'top-right' (react-map-gl parity). */\n    position?: MapControlPosition\n    /** Element to make fullscreen. Defaults to the map container. */\n    container?: HTMLElement\n  }\n</script>\n\n<script lang=\"ts\">\n  /**\n   * Standalone fullscreen toggle — the Svelte equivalent of React's\n   * `MapFullscreenControl` (react-map-gl re-export). Must be rendered inside\n   * `<Map>` children; reads the map instance from context. For the common\n   * case prefer `<Map fullscreen fullscreenPosition={...}>` — combine a\n   * standalone control with `fullscreen={false}` to avoid duplicates.\n   */\n  import { useMap } from './map-context.svelte'\n\n  let { position = 'top-right', container }: MapFullscreenControlProps = $props()\n\n  const mapCtx = useMap()\n  let control: mapboxgl.FullscreenControl | null = null\n\n  function teardown(map: mapboxgl.Map | null) {\n    if (map && control) {\n      try {\n        map.removeControl(control)\n      } catch {\n        /* already removed */\n      }\n    }\n    control = null\n  }\n\n  $effect(() => {\n    const map = mapCtx.map\n    // Mapbox controls are immutable — recreate when options change.\n    void [position, container]\n    if (!map) return\n    teardown(map)\n    control = new mapboxgl.FullscreenControl(container ? { container } : undefined)\n    map.addControl(control, position)\n    return () => teardown(map)\n  })\n</script>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/map/MapFullscreenControl.svelte"
    },
    {
      "path": "packages/registry-svelte/components/map/map-context.svelte.ts",
      "content": "import { getContext, setContext } from 'svelte'\nimport type mapboxgl from 'mapbox-gl'\n\nexport const MAP_KEY = 'uipkge-map'\nexport const MAP_SOURCE_ID_KEY = 'uipkge-map-source-id'\n\n/** Reactive view of the enclosing `<Map>` instance. Reads track, so an\n *  `$effect` re-runs when the map is created. */\nexport interface MapContext {\n  readonly map: mapboxgl.Map | null\n}\n\n/** Published by `<Map>` once per instance. Must be called during init. */\nexport function setMapContext(ctx: MapContext): void {\n  setContext(MAP_KEY, ctx)\n}\n\n/** Injects the map context published by the enclosing `<Map>`. */\nexport function useMap(): MapContext {\n  const ctx = getContext<MapContext>(MAP_KEY)\n  if (!ctx) {\n    throw new Error('uipkge: Map* components must be rendered inside <Map>.')\n  }\n  return ctx\n}\n\n/** Published by `<MapSource>` so nested `<MapLayer>` picks up its source id. */\nexport function setMapSourceId(id: string): void {\n  setContext(MAP_SOURCE_ID_KEY, id)\n}\n\n/** The enclosing `<MapSource>` id, if any. */\nexport function useMapSourceId(): string | undefined {\n  return getContext<string | undefined>(MAP_SOURCE_ID_KEY) ?? undefined\n}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/map/map-context.svelte.ts"
    },
    {
      "path": "packages/registry-svelte/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/lib/components/ui/map/map.variants.ts"
    },
    {
      "path": "packages/registry-svelte/components/map/index.ts",
      "content": "export { default as Map, type MapProps, type MapRef, type MapControlPosition } from './Map.svelte'\n\nexport { mapVariants, MAPBOX_STYLES, type MapVariant, type MapVariants } from './map.variants'\n\nexport { default as MapSource, type MapSourceProps } from './MapSource.svelte'\nexport { default as MapLayer, type MapLayerProps } from './MapLayer.svelte'\nexport { default as MapMarker, type MapMarkerProps } from './MapMarker.svelte'\nexport { default as MapPopup, type MapPopupProps } from './MapPopup.svelte'\nexport { default as MapNavigationControl, type MapNavigationControlProps } from './MapNavigationControl.svelte'\nexport { default as MapFullscreenControl, type MapFullscreenControlProps } from './MapFullscreenControl.svelte'\n\nexport { useMap } from './map-context.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/map/index.ts"
    },
    {
      "path": "packages/registry-svelte/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);\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(var(--opacity-muted, 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: var(--spacing-2) var(--spacing-3);\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);\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/lib/components/ui/map/map.css"
    }
  ],
  "dependencies": [
    "mapbox-gl",
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "A thin, theme-aware Mapbox GL JS wrapper. Pass an access token and drop MapMarker / MapPopup / MapSource / 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.",
  "categories": [
    "data-display"
  ]
}