{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-geo-map",
  "type": "registry:component",
  "title": "Geo map",
  "description": "Places and routes on a real tiled map, with an accessible list of every place.",
  "registryDependencies": [
    "https://r.assistant-ui.com/elements-surfaces.json"
  ],
  "dependencies": [
    "leaflet"
  ],
  "devDependencies": [
    "@types/leaflet"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/geo-map.tsx",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/elements/geo-map.tsx",
      "content": "\"use client\";\n\nimport \"leaflet/dist/leaflet.css\";\n\nimport type { Map as LeafletMap, Marker as LeafletMarker } from \"leaflet\";\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { field, mono, paper } from \"./surfaces\";\n\nexport interface GeoMapPlace {\n  id: string;\n  lat: number;\n  lng: number;\n  label: string;\n  description?: string | undefined;\n}\n\nexport interface GeoMapRoute {\n  id: string;\n  points: readonly (readonly [lat: number, lng: number])[];\n  label?: string | undefined;\n}\n\nexport interface GeoMapProps extends Omit<\n  ComponentProps<\"div\">,\n  \"children\" | \"onSelect\"\n> {\n  places: readonly GeoMapPlace[];\n  routes?: readonly GeoMapRoute[] | undefined;\n  selectedId?: string | undefined;\n  onSelect?: ((id: string) => void) | undefined;\n  theme?: \"light\" | \"dark\" | \"auto\" | undefined;\n  tileUrl?: string | undefined;\n  attribution?: string | undefined;\n  height?: number | undefined;\n}\n\ntype LeafletModule = typeof import(\"leaflet\");\ntype Coordinate = [lat: number, lng: number];\n\nconst DEFAULT_TILE_URL = \"https://tile.openstreetmap.org/{z}/{x}/{y}.png\";\n\nconst DEFAULT_ATTRIBUTION = \"© OpenStreetMap contributors\";\n\nfunction isCoordinate(lat: number, lng: number) {\n  return (\n    Number.isFinite(lat) &&\n    Number.isFinite(lng) &&\n    lat >= -90 &&\n    lat <= 90 &&\n    lng >= -180 &&\n    lng <= 180\n  );\n}\n\nfunction markerIcon(leaflet: LeafletModule, selected: boolean) {\n  const size = selected ? 16 : 10;\n  return leaflet.divIcon({\n    className: \"geo-map-marker\",\n    html: selected\n      ? '<span class=\"block size-4 rounded-full bg-blue-500 ring-2 ring-background\"></span>'\n      : '<span class=\"block size-2.5 rounded-full bg-foreground/55 ring-2 ring-background\"></span>',\n    iconSize: [size, size],\n    iconAnchor: [size / 2, size / 2],\n  });\n}\n\nfunction useDarkTheme(theme: \"light\" | \"dark\" | \"auto\") {\n  const subscribe = useCallback(\n    (notify: () => void) => {\n      if (theme !== \"auto\" || typeof window === \"undefined\") return () => {};\n\n      const media =\n        typeof window.matchMedia === \"function\"\n          ? window.matchMedia(\"(prefers-color-scheme: dark)\")\n          : null;\n      const observer =\n        typeof MutationObserver === \"function\"\n          ? new MutationObserver(notify)\n          : null;\n      observer?.observe(document.documentElement, {\n        attributes: true,\n        attributeFilter: [\"class\"],\n      });\n      media?.addEventListener(\"change\", notify);\n\n      return () => {\n        observer?.disconnect();\n        media?.removeEventListener(\"change\", notify);\n      };\n    },\n    [theme],\n  );\n  const getSnapshot = useCallback(() => {\n    if (theme === \"dark\") return true;\n    if (theme === \"light\" || typeof document === \"undefined\") return false;\n    return (\n      document.documentElement.classList.contains(\"dark\") ||\n      (typeof window.matchMedia === \"function\" &&\n        window.matchMedia(\"(prefers-color-scheme: dark)\").matches)\n    );\n  }, [theme]);\n\n  return useSyncExternalStore(subscribe, getSnapshot, () => theme === \"dark\");\n}\n\nexport function GeoMap({\n  places,\n  routes,\n  selectedId,\n  onSelect,\n  theme = \"auto\",\n  tileUrl,\n  attribution,\n  height = 260,\n  className,\n  ...props\n}: GeoMapProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const mapRef = useRef<LeafletMap | null>(null);\n  const leafletRef = useRef<LeafletModule | null>(null);\n  const markersRef = useRef(new Map<string, LeafletMarker>());\n  const [mapReady, setMapReady] = useState(false);\n  const mapContent = useMemo(() => {\n    const validPlaces = places.filter((place) =>\n      isCoordinate(place.lat, place.lng),\n    );\n    const routePoints: Coordinate[] = [];\n    const validRoutes =\n      routes\n        ?.map((route) => {\n          const points = route.points\n            .filter(([lat, lng]) => isCoordinate(lat, lng))\n            .map(([lat, lng]) => [lat, lng] as Coordinate);\n          routePoints.push(...points);\n          return { ...route, points };\n        })\n        .filter((route) => route.points.length > 1) ?? [];\n\n    return {\n      signature: JSON.stringify([\n        places.map((place) => [\n          place.id,\n          place.lat,\n          place.lng,\n          place.label,\n          place.description,\n        ]),\n        routes?.map((route) => [route.id, route.points]) ?? [],\n      ]),\n      validPlaces,\n      routePoints,\n      validRoutes,\n    };\n  }, [places, routes]);\n  const { signature: contentSignature, validPlaces } = mapContent;\n  const mapContentRef = useRef(mapContent);\n  mapContentRef.current = mapContent;\n  const resolvedTileUrl = tileUrl ?? DEFAULT_TILE_URL;\n  const resolvedAttribution = attribution ?? DEFAULT_ATTRIBUTION;\n  const [uncontrolledSelectedId, setUncontrolledSelectedId] = useState<\n    string | undefined\n  >(() => validPlaces[0]?.id);\n  const activeId =\n    selectedId ??\n    (validPlaces.some((place) => place.id === uncontrolledSelectedId)\n      ? uncontrolledSelectedId\n      : validPlaces[0]?.id);\n  const activeIdRef = useRef(activeId);\n  activeIdRef.current = activeId;\n  const dark = useDarkTheme(theme);\n\n  const selectPlace = useCallback(\n    (place: GeoMapPlace) => {\n      if (selectedId === undefined) setUncontrolledSelectedId(place.id);\n      onSelect?.(place.id);\n      mapRef.current?.panTo([place.lat, place.lng]);\n    },\n    [onSelect, selectedId],\n  );\n  const selectPlaceRef = useRef(selectPlace);\n  selectPlaceRef.current = selectPlace;\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return undefined;\n\n    let cancelled = false;\n    let map: LeafletMap | null = null;\n    setMapReady(false);\n    void (async () => {\n      const leaflet = await import(\"leaflet\");\n      if (cancelled) return;\n\n      leafletRef.current = leaflet;\n      map = leaflet.map(container, { keyboard: true });\n      mapRef.current = map;\n\n      leaflet\n        .tileLayer(resolvedTileUrl, {\n          attribution: resolvedAttribution,\n        })\n        .addTo(map);\n      setMapReady(true);\n    })();\n\n    return () => {\n      cancelled = true;\n      if (mapRef.current === map) mapRef.current = null;\n      map?.remove();\n    };\n  }, [resolvedAttribution, resolvedTileUrl]);\n\n  useEffect(() => {\n    const leaflet = leafletRef.current;\n    const map = mapRef.current;\n    if (!leaflet || !map || !mapReady) return;\n\n    const { routePoints, validPlaces, validRoutes } = mapContentRef.current;\n    const layer = leaflet.layerGroup().addTo(map);\n    const points: Coordinate[] = [\n      ...validPlaces.map((place) => [place.lat, place.lng] as Coordinate),\n      ...routePoints,\n    ];\n    const [first] = points;\n    if (points.length === 1 && first) {\n      map.setView(first, 13);\n    } else if (points.length > 1) {\n      map.fitBounds(leaflet.latLngBounds(points), { padding: [24, 24] });\n    } else {\n      map.setView([0, 0], 2);\n    }\n\n    for (const route of validRoutes) {\n      leaflet\n        .polyline(route.points, {\n          className: \"stroke-foreground/70\",\n          weight: 2,\n        })\n        .addTo(layer);\n    }\n\n    for (const place of validPlaces) {\n      const marker = leaflet\n        .marker([place.lat, place.lng], {\n          icon: markerIcon(leaflet, place.id === activeIdRef.current),\n          title: place.label,\n        })\n        .addTo(layer)\n        .on(\"click\", () => selectPlaceRef.current(place));\n      markersRef.current.set(place.id, marker);\n    }\n\n    return () => {\n      markersRef.current.clear();\n      layer.remove();\n    };\n  }, [contentSignature, mapReady]);\n\n  useEffect(() => {\n    const leaflet = leafletRef.current;\n    if (!leaflet) return;\n\n    for (const place of mapContentRef.current.validPlaces) {\n      markersRef.current\n        .get(place.id)\n        ?.setIcon(markerIcon(leaflet, place.id === activeId));\n    }\n  }, [activeId, contentSignature]);\n\n  return (\n    <div\n      data-slot=\"geo-map\"\n      className={cn(\n        paper,\n        \"flex w-full max-w-sm flex-col overflow-hidden rounded-2xl p-2\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        ref={containerRef}\n        role=\"region\"\n        aria-label={`Map of ${validPlaces.length} places`}\n        style={{ height }}\n        data-theme={dark ? \"dark\" : \"light\"}\n        className={cn(\n          field,\n          \"relative overflow-hidden rounded-xl\",\n          \"[&_.leaflet-bar]:border-foreground/10! [&_.leaflet-bar_a]:border-foreground/10! [&_.leaflet-bar_a]:bg-background! [&_.leaflet-bar_a]:text-foreground/80! [&_.leaflet-bar_a:hover]:bg-foreground/[0.06]! [&_.leaflet-bar]:shadow-none!\",\n          \"[&_.leaflet-control-attribution]:bg-background/80! [&_.leaflet-control-attribution]:text-foreground/55! [&_.leaflet-control-attribution_a]:text-foreground/70!\",\n          tileUrl === undefined &&\n            (dark\n              ? \"[&_.leaflet-tile-pane]:brightness-90 [&_.leaflet-tile-pane]:contrast-90 [&_.leaflet-tile-pane]:grayscale [&_.leaflet-tile-pane]:hue-rotate-180 [&_.leaflet-tile-pane]:invert\"\n              : \"[&_.leaflet-tile-pane]:contrast-90 [&_.leaflet-tile-pane]:grayscale\"),\n        )}\n      >\n        {!mapReady ? (\n          <span className=\"text-foreground/45 absolute inset-0 flex items-center justify-center text-xs\">\n            Loading map\n          </span>\n        ) : null}\n      </div>\n\n      <ol className=\"mt-2 flex flex-col gap-0.5\">\n        {validPlaces.map((place) => {\n          const selected = place.id === activeId;\n          return (\n            <li key={place.id}>\n              <button\n                type=\"button\"\n                aria-current={selected || undefined}\n                onClick={() => selectPlace(place)}\n                className={cn(\n                  \"hover:bg-foreground/[0.035] focus-visible:ring-foreground/20 flex w-full items-start gap-3 rounded-lg px-2.5 py-2 text-start transition-colors outline-none focus-visible:ring-1\",\n                  selected && \"bg-foreground/[0.06]\",\n                )}\n              >\n                <span className=\"min-w-0 flex-1\">\n                  <span className=\"text-foreground/90 block text-[13.5px] leading-5 break-words\">\n                    {place.label}\n                  </span>\n                  {place.description ? (\n                    <span className=\"text-foreground/45 block text-xs leading-4 break-words\">\n                      {place.description}\n                    </span>\n                  ) : null}\n                </span>\n                <span\n                  className={cn(\n                    mono,\n                    \"text-foreground/35 shrink-0 pt-0.5 tabular-nums\",\n                  )}\n                >\n                  {place.lat.toFixed(4)}, {place.lng.toFixed(4)}\n                </span>\n              </button>\n            </li>\n          );\n        })}\n      </ol>\n    </div>\n  );\n}\n"
    }
  ]
}