{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "geo-map",
  "type": "registry:block",
  "title": "Geo Map",
  "description": "Display geolocated entities and fleet positions.",
  "dependencies": [
    "leaflet",
    "react-leaflet",
    "supercluster",
    "zod"
  ],
  "files": [
    {
      "path": "components/tool-ui/geo-map/_adapter.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/geo-map/_adapter.tsx",
      "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn      → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Leaflet → map primitives from react-leaflet\n */\n\nexport { cn } from \"@/lib/utils\";\nexport {\n  CircleMarker,\n  MapContainer,\n  Marker,\n  Polyline,\n  Popup,\n  TileLayer,\n  Tooltip,\n  ZoomControl,\n  useMap,\n  useMapEvents,\n} from \"react-leaflet\";\n"
    },
    {
      "path": "components/tool-ui/geo-map/geo-map-engine.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/geo-map/geo-map-engine.tsx",
      "content": "\"use client\";\n\nimport type { Map as LeafletMap } from \"leaflet\";\nimport { memo, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport Supercluster from \"supercluster\";\nimport {\n  CircleMarker,\n  MapContainer,\n  Marker,\n  Polyline,\n  TileLayer,\n  ZoomControl,\n  useMap,\n  useMapEvents,\n} from \"./_adapter\";\nimport { createClusterIcon, resolveMarkerIcon } from \"./geo-map-icons\";\nimport { GeoMapOverlays } from \"./geo-map-overlays\";\nimport type {\n  GeoMapClustering,\n  GeoMapFitTarget,\n  GeoMapMarker,\n  GeoMapRoute,\n  GeoMapViewport,\n} from \"./schema\";\n\nconst TILE_ATTRIBUTION =\n  '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors &copy; <a href=\"https://carto.com/attributions\">CARTO</a>';\nconst ROUTE_DEFAULT_COLOR = \"var(--primary)\";\nconst ROUTE_DEFAULT_WEIGHT = 3;\nconst ROUTE_DEFAULT_OPACITY = 0.85;\nconst EMPTY_ROUTES: GeoMapRoute[] = [];\n\nconst CLUSTER_RADIUS_DEFAULT = 60;\nconst CLUSTER_MAX_ZOOM_DEFAULT = 16;\nconst CLUSTER_MIN_POINTS_DEFAULT = 2;\n\nconst DEFAULT_CENTER: [number, number] = [20, 0];\nexport const DEFAULT_VIEW_ZOOM = 2;\nconst SINGLE_LOCATION_ZOOM = 13;\nconst DEFAULT_VIEWPORT_PADDING = 32;\n\ntype LeafletRuntime = Pick<\n  typeof import(\"leaflet\"),\n  \"divIcon\" | \"latLngBounds\"\n>;\n\nexport type GeoMapBbox = [\n  west: number,\n  south: number,\n  east: number,\n  north: number,\n];\nexport type GeoMapLatLng = [lat: number, lng: number];\n\nexport type GeoMapClusterProperties = {\n  cluster?: boolean;\n  cluster_id?: number;\n  point_count?: number;\n  markerId?: string;\n};\n\nexport type GeoMapClusterFeature = GeoJSON.Feature<\n  GeoJSON.Point,\n  GeoMapClusterProperties\n>;\n\ntype MarkerClusterPointProperties = GeoMapClusterProperties & {\n  markerId?: string;\n  marker?: GeoMapMarker;\n};\n\ntype MapViewportState = {\n  bbox: GeoMapBbox;\n  zoom: number;\n};\n\nfunction roundCoordinate(value: number): number {\n  return Math.round(value * 1_000_000) / 1_000_000;\n}\n\nfunction normalizeViewportState(state: MapViewportState): MapViewportState {\n  return {\n    bbox: [\n      roundCoordinate(state.bbox[0]),\n      roundCoordinate(state.bbox[1]),\n      roundCoordinate(state.bbox[2]),\n      roundCoordinate(state.bbox[3]),\n    ],\n    zoom: state.zoom,\n  };\n}\n\nfunction areViewportStatesEqual(\n  a: MapViewportState | null,\n  b: MapViewportState,\n): boolean {\n  if (!a) {\n    return false;\n  }\n\n  return (\n    a.zoom === b.zoom &&\n    a.bbox[0] === b.bbox[0] &&\n    a.bbox[1] === b.bbox[1] &&\n    a.bbox[2] === b.bbox[2] &&\n    a.bbox[3] === b.bbox[3]\n  );\n}\n\nfunction serializeFitPoints(points: [number, number][]): string {\n  return points\n    .map(([lat, lng]) => `${roundCoordinate(lat)},${roundCoordinate(lng)}`)\n    .join(\"|\");\n}\n\nfunction readViewportState(map: LeafletMap): MapViewportState {\n  const bounds = map.getBounds();\n  return normalizeViewportState({\n    bbox: [\n      bounds.getWest(),\n      bounds.getSouth(),\n      bounds.getEast(),\n      bounds.getNorth(),\n    ],\n    zoom: Math.round(map.getZoom()),\n  });\n}\n\nexport function collectFitPoints(\n  markers: GeoMapMarker[],\n  routes: GeoMapRoute[],\n  target: GeoMapFitTarget,\n): GeoMapLatLng[] {\n  const markerPoints =\n    target === \"markers\" || target === \"all\"\n      ? markers.map((marker) => [marker.lat, marker.lng] as GeoMapLatLng)\n      : [];\n\n  const routePoints =\n    target === \"routes\" || target === \"all\"\n      ? routes.flatMap((route) =>\n          route.points.map((point) => [point.lat, point.lng] as GeoMapLatLng),\n        )\n      : [];\n\n  return [...markerPoints, ...routePoints];\n}\n\nexport function resolveFitPointsWithFallback(\n  markers: GeoMapMarker[],\n  routes: GeoMapRoute[],\n  target: GeoMapFitTarget,\n): GeoMapLatLng[] {\n  const selected = collectFitPoints(markers, routes, target);\n  if (selected.length > 0) {\n    return selected;\n  }\n\n  if (target !== \"markers\") {\n    return collectFitPoints(markers, routes, \"markers\");\n  }\n\n  return [];\n}\n\nexport function splitDatelineBbox(bbox: GeoMapBbox): GeoMapBbox[] {\n  const [west, south, east, north] = bbox;\n\n  if (west <= east) {\n    return [bbox];\n  }\n\n  return [\n    [west, south, 180, north],\n    [-180, south, east, north],\n  ];\n}\n\nfunction getClusterFeatureKey(feature: GeoMapClusterFeature): string {\n  const properties = feature.properties ?? {};\n\n  if (properties.cluster && typeof properties.cluster_id === \"number\") {\n    return `cluster:${properties.cluster_id}`;\n  }\n\n  if (\n    typeof properties.markerId === \"string\" &&\n    properties.markerId.length > 0\n  ) {\n    return `marker:${properties.markerId}`;\n  }\n\n  if (feature.id !== undefined && feature.id !== null) {\n    return `id:${String(feature.id)}`;\n  }\n\n  const [lng, lat] = feature.geometry.coordinates;\n  return `point:${lat}:${lng}`;\n}\n\nfunction dedupeClusterFeatures(\n  features: GeoMapClusterFeature[],\n): GeoMapClusterFeature[] {\n  const seen = new Set<string>();\n  const deduped: GeoMapClusterFeature[] = [];\n\n  features.forEach((feature) => {\n    const key = getClusterFeatureKey(feature);\n    if (seen.has(key)) {\n      return;\n    }\n\n    seen.add(key);\n    deduped.push(feature);\n  });\n\n  return deduped;\n}\n\nexport function getClustersForDatelineAwareBbox(\n  bbox: GeoMapBbox,\n  zoom: number,\n  getClustersForBbox: (\n    candidateBbox: GeoMapBbox,\n    zoom: number,\n  ) => GeoMapClusterFeature[],\n): GeoMapClusterFeature[] {\n  const queried = splitDatelineBbox(bbox).flatMap((candidateBbox) =>\n    getClustersForBbox(candidateBbox, zoom),\n  );\n\n  return dedupeClusterFeatures(queried);\n}\n\nexport function toSafeExpansionZoom(\n  zoom: number,\n  options?: { minZoom?: number; maxZoom?: number; fallback?: number },\n): number {\n  const minZoom = options?.minZoom ?? 1;\n  const maxZoom = options?.maxZoom ?? 22;\n  const fallback = options?.fallback ?? 2;\n\n  if (!Number.isFinite(zoom)) {\n    return fallback;\n  }\n\n  return Math.min(maxZoom, Math.max(minZoom, Math.round(zoom)));\n}\n\nfunction resolveInitialView(\n  markers: GeoMapMarker[],\n  routes: GeoMapRoute[],\n  viewport: GeoMapViewport | undefined,\n): { center: [number, number]; zoom: number } {\n  if (viewport?.mode === \"center\") {\n    return {\n      center: [viewport.center.lat, viewport.center.lng],\n      zoom: viewport.zoom,\n    };\n  }\n\n  const fitTarget = viewport?.target ?? \"all\";\n  const fitPoints = resolveFitPointsWithFallback(markers, routes, fitTarget);\n\n  if (fitPoints.length === 1) {\n    return {\n      center: [fitPoints[0][0], fitPoints[0][1]],\n      zoom: viewport?.maxZoom\n        ? Math.min(SINGLE_LOCATION_ZOOM, viewport.maxZoom)\n        : SINGLE_LOCATION_ZOOM,\n    };\n  }\n\n  return { center: DEFAULT_CENTER, zoom: DEFAULT_VIEW_ZOOM };\n}\n\nfunction ViewportController({\n  markers,\n  routes,\n  viewport,\n  leafletRuntime,\n}: {\n  markers: GeoMapMarker[];\n  routes: GeoMapRoute[];\n  viewport: GeoMapViewport | undefined;\n  leafletRuntime: LeafletRuntime;\n}) {\n  const map = useMap();\n  const lastAppliedViewportRef = useRef<string | null>(null);\n\n  useEffect(() => {\n    lastAppliedViewportRef.current = null;\n  }, [map]);\n\n  useEffect(() => {\n    if (viewport?.mode === \"center\") {\n      const viewportKey = `center:${roundCoordinate(viewport.center.lat)}:${roundCoordinate(viewport.center.lng)}:${viewport.zoom}`;\n      if (lastAppliedViewportRef.current === viewportKey) {\n        return;\n      }\n\n      lastAppliedViewportRef.current = viewportKey;\n      map.setView([viewport.center.lat, viewport.center.lng], viewport.zoom);\n      return;\n    }\n\n    const fitTarget = viewport?.target ?? \"all\";\n    const fitPoints = resolveFitPointsWithFallback(markers, routes, fitTarget);\n    if (fitPoints.length === 0) {\n      return;\n    }\n\n    const maxZoom = viewport?.maxZoom;\n    if (fitPoints.length === 1) {\n      const [lat, lng] = fitPoints[0];\n      const zoom = maxZoom\n        ? Math.min(SINGLE_LOCATION_ZOOM, maxZoom)\n        : SINGLE_LOCATION_ZOOM;\n      const viewportKey = `fit-single:${roundCoordinate(lat)}:${roundCoordinate(lng)}:${zoom}`;\n      if (lastAppliedViewportRef.current === viewportKey) {\n        return;\n      }\n\n      lastAppliedViewportRef.current = viewportKey;\n      map.setView([lat, lng], zoom);\n      return;\n    }\n\n    const padding = viewport?.padding ?? DEFAULT_VIEWPORT_PADDING;\n    const viewportKey = `fit:${fitTarget}:${padding}:${maxZoom ?? \"none\"}:${serializeFitPoints(fitPoints)}`;\n    if (lastAppliedViewportRef.current === viewportKey) {\n      return;\n    }\n\n    lastAppliedViewportRef.current = viewportKey;\n    const bounds = leafletRuntime.latLngBounds(fitPoints);\n    map.fitBounds(bounds, {\n      maxZoom,\n      padding: [padding, padding],\n    });\n  }, [leafletRuntime, map, markers, routes, viewport]);\n\n  return null;\n}\n\nfunction MapObserver({\n  onViewportChange,\n  onMapReady,\n}: {\n  onViewportChange: (state: MapViewportState) => void;\n  onMapReady: (map: LeafletMap) => void;\n}) {\n  const map = useMapEvents({\n    moveend: () => {\n      onViewportChange(readViewportState(map));\n    },\n    zoomend: () => {\n      onViewportChange(readViewportState(map));\n    },\n  });\n\n  useEffect(() => {\n    onMapReady(map);\n    onViewportChange(readViewportState(map));\n  }, [map, onMapReady, onViewportChange]);\n\n  return null;\n}\n\nfunction resolveMarkerAriaLabel(marker: GeoMapMarker): string {\n  if (marker.label && marker.description) {\n    return `${marker.label}. ${marker.description}`;\n  }\n\n  return (\n    marker.label ??\n    marker.description ??\n    `Marker at ${marker.lat.toFixed(4)}, ${marker.lng.toFixed(4)}`\n  );\n}\n\nexport const GeoMapEngine = memo(function GeoMapEngine({\n  id,\n  markers,\n  routes,\n  clustering,\n  viewport,\n  showZoomControl,\n  tileUrl,\n  mapAriaLabel,\n  tooltipClassName,\n  popupClassName,\n  onMarkerClick,\n  onRouteClick,\n  onReadyChange,\n}: {\n  id: string;\n  markers: GeoMapMarker[];\n  routes?: GeoMapRoute[];\n  clustering?: GeoMapClustering;\n  viewport?: GeoMapViewport;\n  showZoomControl: boolean;\n  tileUrl: string;\n  mapAriaLabel: string;\n  tooltipClassName?: string;\n  popupClassName?: string;\n  onMarkerClick?: (marker: GeoMapMarker) => void;\n  onRouteClick?: (route: GeoMapRoute) => void;\n  onReadyChange?: (isReady: boolean) => void;\n}) {\n  const resolvedRoutes = routes ?? EMPTY_ROUTES;\n  const [leafletRuntime, setLeafletRuntime] = useState<LeafletRuntime | null>(\n    null,\n  );\n  const [mapInstance, setMapInstance] = useState<LeafletMap | null>(null);\n  const [viewportState, setViewportState] = useState<MapViewportState | null>(\n    null,\n  );\n\n  const handleViewportChange = useCallback((nextState: MapViewportState) => {\n    const normalized = normalizeViewportState(nextState);\n    setViewportState((previousState) =>\n      areViewportStatesEqual(previousState, normalized)\n        ? previousState\n        : normalized,\n    );\n  }, []);\n\n  useEffect(() => {\n    let isActive = true;\n\n    void import(\"leaflet\").then((module) => {\n      if (!isActive) {\n        return;\n      }\n\n      setLeafletRuntime({\n        divIcon: module.divIcon,\n        latLngBounds: module.latLngBounds,\n      });\n    });\n\n    return () => {\n      isActive = false;\n    };\n  }, []);\n\n  const isReady = leafletRuntime !== null;\n\n  useEffect(() => {\n    onReadyChange?.(isReady);\n  }, [isReady, onReadyChange]);\n\n  useEffect(() => {\n    if (!mapInstance) {\n      return;\n    }\n\n    const container = mapInstance.getContainer();\n    container.setAttribute(\"role\", \"region\");\n    container.setAttribute(\"aria-label\", mapAriaLabel);\n  }, [mapAriaLabel, mapInstance]);\n\n  useEffect(() => {\n    if (!mapInstance) {\n      return;\n    }\n\n    const handleEscape = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        mapInstance.closePopup();\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleEscape);\n    return () => {\n      document.removeEventListener(\"keydown\", handleEscape);\n    };\n  }, [mapInstance]);\n\n  const initialView = useMemo(\n    () => resolveInitialView(markers, resolvedRoutes, viewport),\n    [markers, resolvedRoutes, viewport],\n  );\n\n  const markerById = useMemo(() => {\n    const map = new Map<string, GeoMapMarker>();\n    markers.forEach((marker, index) => {\n      map.set(marker.id ?? `marker-${index}`, marker);\n    });\n    return map;\n  }, [markers]);\n\n  const clusterConfig = useMemo(\n    () => ({\n      enabled: clustering?.enabled === true,\n      radius: clustering?.radius ?? CLUSTER_RADIUS_DEFAULT,\n      maxZoom: clustering?.maxZoom ?? CLUSTER_MAX_ZOOM_DEFAULT,\n      minPoints: clustering?.minPoints ?? CLUSTER_MIN_POINTS_DEFAULT,\n    }),\n    [clustering],\n  );\n\n  const clusterIndex = useMemo(() => {\n    if (!clusterConfig.enabled) {\n      return null;\n    }\n\n    const index = new Supercluster<MarkerClusterPointProperties>({\n      radius: clusterConfig.radius,\n      maxZoom: clusterConfig.maxZoom,\n      minPoints: clusterConfig.minPoints,\n    });\n\n    const points = markers.map((marker, index) => {\n      const markerId = marker.id ?? `marker-${index}`;\n      return {\n        type: \"Feature\" as const,\n        id: markerId,\n        geometry: {\n          type: \"Point\" as const,\n          coordinates: [marker.lng, marker.lat] as [number, number],\n        },\n        properties: {\n          markerId,\n          marker,\n        },\n      };\n    });\n\n    index.load(points);\n    return index;\n  }, [\n    clusterConfig.enabled,\n    clusterConfig.maxZoom,\n    clusterConfig.minPoints,\n    clusterConfig.radius,\n    markers,\n  ]);\n\n  const clusteredFeatures = useMemo(() => {\n    if (!clusterConfig.enabled || !clusterIndex || !viewportState) {\n      return [] as GeoMapClusterFeature[];\n    }\n\n    return getClustersForDatelineAwareBbox(\n      viewportState.bbox,\n      viewportState.zoom,\n      (bbox, zoom) =>\n        clusterIndex.getClusters(bbox, zoom) as GeoMapClusterFeature[],\n    );\n  }, [clusterConfig.enabled, clusterIndex, viewportState]);\n\n  const renderMarker = useCallback(\n    (\n      marker: GeoMapMarker,\n      markerKey: string,\n      markerPositionOverride?: [number, number],\n    ) => {\n      const markerPosition: [number, number] = markerPositionOverride ?? [\n        marker.lat,\n        marker.lng,\n      ];\n      const tooltipMode = marker.tooltip ?? \"hover\";\n      const tooltipContent = marker.label ?? marker.description;\n      const icon = marker.icon;\n      const markerAriaLabel = resolveMarkerAriaLabel(marker);\n\n      if (!leafletRuntime) {\n        return null;\n      }\n\n      const leafletIcon = resolveMarkerIcon(icon, leafletRuntime);\n      if (leafletIcon) {\n        return (\n          <Marker\n            key={markerKey}\n            position={markerPosition}\n            icon={leafletIcon}\n            title={markerAriaLabel}\n            alt={markerAriaLabel}\n            eventHandlers={{\n              click: () => onMarkerClick?.(marker),\n            }}\n          >\n            <GeoMapOverlays\n              tooltipMode={tooltipMode}\n              tooltipContent={tooltipContent}\n              label={marker.label}\n              description={marker.description}\n              tooltipClassName={tooltipClassName}\n              popupClassName={popupClassName}\n            />\n          </Marker>\n        );\n      }\n\n      const markerStroke =\n        icon?.type === \"dot\"\n          ? (icon.borderColor ?? \"var(--border)\")\n          : \"var(--border)\";\n      const markerFill =\n        icon?.type === \"dot\"\n          ? (icon.color ?? \"var(--primary)\")\n          : \"var(--primary)\";\n      const markerRadius = icon?.type === \"dot\" ? (icon.radius ?? 7) : 7;\n\n      return (\n        <CircleMarker\n          key={markerKey}\n          center={markerPosition}\n          radius={markerRadius}\n          pathOptions={{\n            color: markerStroke,\n            fillColor: markerFill,\n            fillOpacity: 0.95,\n            weight: 2,\n          }}\n          eventHandlers={{\n            click: () => onMarkerClick?.(marker),\n          }}\n        >\n          <GeoMapOverlays\n            tooltipMode={tooltipMode}\n            tooltipContent={tooltipContent}\n            label={marker.label}\n            description={marker.description}\n            tooltipClassName={tooltipClassName}\n            popupClassName={popupClassName}\n          />\n        </CircleMarker>\n      );\n    },\n    [leafletRuntime, onMarkerClick, popupClassName, tooltipClassName],\n  );\n\n  if (!leafletRuntime) {\n    return null;\n  }\n\n  return (\n    <MapContainer\n      center={initialView.center}\n      zoom={initialView.zoom}\n      zoomControl={false}\n      className=\"h-full w-full\"\n      scrollWheelZoom\n    >\n      <TileLayer attribution={TILE_ATTRIBUTION} url={tileUrl} />\n      {showZoomControl && <ZoomControl position=\"topright\" />}\n      <MapObserver\n        onMapReady={setMapInstance}\n        onViewportChange={handleViewportChange}\n      />\n      <ViewportController\n        leafletRuntime={leafletRuntime}\n        markers={markers}\n        routes={resolvedRoutes}\n        viewport={viewport}\n      />\n\n      {resolvedRoutes.map((route, routeIndex) => {\n        const routeKey = route.id ?? `${id}-route-${routeIndex}`;\n        const positions = route.points.map((point) => [\n          point.lat,\n          point.lng,\n        ]) as [number, number][];\n        const tooltipMode = route.tooltip ?? \"hover\";\n        const tooltipContent = route.label ?? route.description;\n\n        return (\n          <Polyline\n            key={routeKey}\n            positions={positions}\n            pathOptions={{\n              color: route.color ?? ROUTE_DEFAULT_COLOR,\n              weight: route.weight ?? ROUTE_DEFAULT_WEIGHT,\n              opacity: route.opacity ?? ROUTE_DEFAULT_OPACITY,\n              dashArray: route.dashArray,\n            }}\n            eventHandlers={{\n              click: () => onRouteClick?.(route),\n            }}\n          >\n            <GeoMapOverlays\n              tooltipMode={tooltipMode}\n              tooltipContent={tooltipContent}\n              label={route.label}\n              description={route.description}\n              tooltipClassName={tooltipClassName}\n              popupClassName={popupClassName}\n            />\n          </Polyline>\n        );\n      })}\n\n      {clusterConfig.enabled && clusterIndex && viewportState\n        ? clusteredFeatures.map((feature, index) => {\n            const [lng, lat] = feature.geometry.coordinates;\n            const properties = (feature.properties ??\n              {}) as MarkerClusterPointProperties;\n\n            if (\n              properties.cluster &&\n              typeof properties.cluster_id === \"number\"\n            ) {\n              const pointCount = properties.point_count ?? 0;\n              const clusterId = properties.cluster_id;\n              const clusterIcon = createClusterIcon(pointCount, leafletRuntime);\n              const clusterAriaLabel = `Cluster containing ${pointCount} locations`;\n\n              return (\n                <Marker\n                  key={`cluster-${clusterId}`}\n                  position={[lat, lng]}\n                  icon={clusterIcon}\n                  title={clusterAriaLabel}\n                  alt={clusterAriaLabel}\n                  eventHandlers={{\n                    click: () => {\n                      if (!mapInstance) {\n                        return;\n                      }\n\n                      const expansionZoom = toSafeExpansionZoom(\n                        clusterIndex.getClusterExpansionZoom(clusterId),\n                        {\n                          maxZoom: 22,\n                          fallback:\n                            (viewportState.zoom ?? DEFAULT_VIEW_ZOOM) + 2,\n                        },\n                      );\n                      mapInstance.flyTo([lat, lng], expansionZoom);\n                    },\n                  }}\n                />\n              );\n            }\n\n            const marker =\n              properties.marker ??\n              markerById.get(properties.markerId ?? `marker-${index}`);\n            if (!marker) {\n              return null;\n            }\n\n            const markerKey =\n              marker.id ?? properties.markerId ?? `${id}-cluster-leaf-${index}`;\n            return renderMarker(marker, markerKey, [lat, lng]);\n          })\n        : markers.map((marker, index) =>\n            renderMarker(marker, marker.id ?? `${id}-marker-${index}`),\n          )}\n    </MapContainer>\n  );\n});\n"
    },
    {
      "path": "components/tool-ui/geo-map/geo-map-icons.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/geo-map/geo-map-icons.ts",
      "content": "import type { DivIcon } from \"leaflet\";\nimport type { GeoMapMarker } from \"./schema\";\n\ntype LeafletIconRuntime = Pick<typeof import(\"leaflet\"), \"divIcon\">;\n\nfunction isSafeHttpUrl(value: string | undefined): boolean {\n  if (!value) {\n    return false;\n  }\n\n  try {\n    const parsed = new URL(value);\n    return parsed.protocol === \"http:\" || parsed.protocol === \"https:\";\n  } catch {\n    return false;\n  }\n}\n\nfunction escapeHtml(value: string): string {\n  return value\n    .replaceAll(\"&\", \"&amp;\")\n    .replaceAll(\"<\", \"&lt;\")\n    .replaceAll(\">\", \"&gt;\")\n    .replaceAll('\"', \"&quot;\")\n    .replaceAll(\"'\", \"&#39;\");\n}\n\nfunction createEmojiIcon(\n  icon: Extract<NonNullable<GeoMapMarker[\"icon\"]>, { type: \"emoji\" }>,\n  leafletRuntime: LeafletIconRuntime,\n): DivIcon {\n  const size = icon.size ?? 24;\n  const background = icon.bgColor ?? \"var(--card)\";\n  const border = icon.borderColor ?? \"var(--border)\";\n\n  return leafletRuntime.divIcon({\n    className: \"\",\n    html: `<span style=\"\ndisplay:flex;\nalign-items:center;\njustify-content:center;\nwidth:${size}px;\nheight:${size}px;\nborder-radius:999px;\nbackground:${background};\nborder:1px solid ${border};\nfont-size:${Math.round(size * 0.62)}px;\nline-height:1;\nbox-shadow:0 1px 3px oklch(from var(--foreground) l c h / 0.22);\n\">${escapeHtml(icon.value)}</span>`,\n    iconSize: [size, size],\n    iconAnchor: [size / 2, size / 2],\n    popupAnchor: [0, -Math.round(size / 2)],\n    tooltipAnchor: [0, -Math.round(size / 2)],\n  });\n}\n\nfunction createImageIcon(\n  icon: Extract<NonNullable<GeoMapMarker[\"icon\"]>, { type: \"image\" }>,\n  leafletRuntime: LeafletIconRuntime,\n): DivIcon {\n  const width = icon.width ?? 28;\n  const height = icon.height ?? 28;\n  const borderRadius = icon.borderRadius ?? Math.min(width, height) / 2;\n  const border = icon.borderColor ?? \"var(--border)\";\n\n  return leafletRuntime.divIcon({\n    className: \"\",\n    html: `<span style=\"\ndisplay:block;\nwidth:${width}px;\nheight:${height}px;\nborder-radius:${borderRadius}px;\noverflow:hidden;\nborder:1px solid ${border};\nbackground:var(--card);\nbox-shadow:0 1px 3px oklch(from var(--foreground) l c h / 0.22);\n\"><img src=\"${escapeHtml(icon.url)}\" alt=\"\" style=\"width:100%;height:100%;object-fit:cover;display:block;\" /></span>`,\n    iconSize: [width, height],\n    iconAnchor: [width / 2, height / 2],\n    popupAnchor: [0, -Math.round(height / 2)],\n    tooltipAnchor: [0, -Math.round(height / 2)],\n  });\n}\n\nexport function createClusterIcon(\n  count: number,\n  leafletRuntime: LeafletIconRuntime,\n): DivIcon {\n  const size = count >= 100 ? 42 : count >= 10 ? 38 : 34;\n  const background = \"var(--primary)\";\n  const border = \"var(--background)\";\n\n  return leafletRuntime.divIcon({\n    className: \"\",\n    html: `<span style=\"\ndisplay:flex;\nalign-items:center;\njustify-content:center;\nwidth:${size}px;\nheight:${size}px;\nborder-radius:999px;\nbackground:${background};\nborder:2px solid ${border};\ncolor:var(--primary-foreground);\nfont-size:12px;\nfont-weight:700;\nline-height:1;\nbox-shadow:0 2px 6px oklch(from var(--foreground) l c h / 0.25);\n\">${count}</span>`,\n    iconSize: [size, size],\n    iconAnchor: [size / 2, size / 2],\n    popupAnchor: [0, -Math.round(size / 2)],\n    tooltipAnchor: [0, -Math.round(size / 2)],\n  });\n}\n\nexport function resolveMarkerIcon(\n  icon: GeoMapMarker[\"icon\"] | undefined,\n  leafletRuntime: LeafletIconRuntime,\n): DivIcon | null {\n  if (icon?.type === \"emoji\") {\n    return createEmojiIcon(icon, leafletRuntime);\n  }\n\n  if (icon?.type === \"image\" && isSafeHttpUrl(icon.url)) {\n    return createImageIcon(icon, leafletRuntime);\n  }\n\n  return null;\n}\n"
    },
    {
      "path": "components/tool-ui/geo-map/geo-map-overlays.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/geo-map/geo-map-overlays.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useState } from \"react\";\n\nimport { Popup, Tooltip, cn } from \"./_adapter\";\n\nfunction GeoMapPopupContent({\n  label,\n  description,\n}: {\n  label?: string;\n  description?: string;\n}) {\n  return (\n    <div className=\"flex flex-col gap-0.5\">\n      {label && (\n        <p className=\"block text-sm leading-tight font-semibold tracking-tight text-foreground\">\n          {label}\n        </p>\n      )}\n      {description && (\n        <p className=\"block text-xs leading-relaxed text-muted-foreground\">\n          {description}\n        </p>\n      )}\n    </div>\n  );\n}\n\nfunction GeoMapTooltipContent({ text }: { text: string }) {\n  return <span className=\"block\">{text}</span>;\n}\n\nexport function GeoMapOverlays({\n  tooltipMode,\n  tooltipContent,\n  label,\n  description,\n  tooltipClassName,\n  popupClassName,\n}: {\n  tooltipMode: \"none\" | \"hover\" | \"always\";\n  tooltipContent?: string;\n  label?: string;\n  description?: string;\n  tooltipClassName?: string;\n  popupClassName?: string;\n}) {\n  const hasPopup = Boolean(label || description);\n  const [isPopupOpen, setIsPopupOpen] = useState(false);\n  const shouldRenderTooltip =\n    tooltipMode !== \"none\" && tooltipContent && (!hasPopup || !isPopupOpen);\n  const popupEventHandlers = useMemo(\n    () => ({\n      add: () => setIsPopupOpen(true),\n      remove: () => setIsPopupOpen(false),\n    }),\n    [],\n  );\n\n  return (\n    <>\n      {shouldRenderTooltip && (\n        <Tooltip\n          direction=\"top\"\n          permanent={tooltipMode === \"always\"}\n          className={cn(\"geo-map-tooltip\", tooltipClassName)}\n        >\n          <GeoMapTooltipContent text={tooltipContent} />\n        </Tooltip>\n      )}\n      {hasPopup && (\n        <Popup\n          className={cn(\"geo-map-popup\", popupClassName)}\n          closeButton\n          closeOnEscapeKey\n          minWidth={0}\n          maxWidth={288}\n          eventHandlers={popupEventHandlers}\n        >\n          <GeoMapPopupContent label={label} description={description} />\n        </Popup>\n      )}\n    </>\n  );\n}\n"
    },
    {
      "path": "components/tool-ui/geo-map/geo-map-theme.module.css",
      "type": "registry:style",
      "target": "components/tool-ui/geo-map/geo-map-theme.module.css",
      "content": ".root[data-slot=\"geo-map\"] {\n  --geo-map-canvas-bg: var(--muted);\n  --geo-map-tooltip-bg: var(--foreground);\n  --geo-map-tooltip-fg: var(--background);\n  --geo-map-tooltip-shadow: 0 8px 20px\n    oklch(from var(--foreground) l c h / 0.18);\n  --geo-map-tooltip-radius: calc(var(--radius) - 2px);\n  --geo-map-tooltip-padding: 0.375rem 0.625rem;\n  --geo-map-tooltip-font-size: 0.75rem;\n  --geo-map-tooltip-font-weight: 500;\n  --geo-map-tooltip-line-height: 1.2;\n  --geo-map-popup-margin-bottom: 12px;\n  --geo-map-popup-border: var(--border);\n  --geo-map-popup-radius: calc(var(--radius) + 2px);\n  --geo-map-popup-bg: oklch(from var(--popover) l c h / 0.96);\n  --geo-map-popup-fg: var(--popover-foreground);\n  --geo-map-popup-shadow: 0 10px 30px oklch(from var(--foreground) l c h / 0.12);\n  --geo-map-popup-blur: 8px;\n  --geo-map-popup-content-padding: 0.625rem 0.75rem;\n  --geo-map-popup-max-width: min(80vw, 18rem);\n  --geo-map-popup-font-family: var(\n    --font-sans,\n    ui-sans-serif,\n    system-ui,\n    sans-serif\n  );\n  --geo-map-zoom-bg: oklch(from var(--background) l c h / 0.78);\n  --geo-map-zoom-fg: var(--foreground);\n  --geo-map-zoom-border: var(--border);\n  --geo-map-zoom-hover-bg: oklch(from var(--accent) l c h / 0.82);\n  --geo-map-zoom-hover-fg: var(--accent-foreground);\n  --geo-map-zoom-disabled-bg: oklch(from var(--muted) l c h / 0.72);\n  --geo-map-zoom-disabled-fg: var(--muted-foreground);\n  --geo-map-zoom-shadow: 0 1px 2px oklch(from var(--foreground) l c h / 0.08);\n  --geo-map-zoom-focus-ring: var(--ring);\n  --geo-map-zoom-radius: 0.5rem;\n  --geo-map-zoom-size: 2.25rem;\n  --geo-map-zoom-font-size: 1.125rem;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-container) {\n  background: var(--geo-map-canvas-bg);\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom) {\n  border: 1px solid var(--geo-map-zoom-border);\n  box-shadow: var(--geo-map-zoom-shadow);\n  background: var(--geo-map-zoom-bg);\n  backdrop-filter: blur(var(--geo-map-popup-blur));\n  -webkit-backdrop-filter: blur(var(--geo-map-popup-blur));\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom.leaflet-bar) {\n  border-radius: var(--geo-map-zoom-radius) !important;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a) {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: var(--geo-map-zoom-size);\n  height: var(--geo-map-zoom-size);\n  line-height: 1;\n  text-indent: 0;\n  border: 0;\n  background: transparent;\n  color: var(--geo-map-zoom-fg);\n  font-size: var(--geo-map-zoom-font-size);\n  font-weight: 500;\n  box-shadow: none;\n  cursor: default;\n  transition:\n    background-color 150ms ease,\n    color 150ms ease,\n    border-color 150ms ease,\n    box-shadow 150ms ease,\n    opacity 150ms ease;\n  border-radius: 0 !important;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a + a) {\n  border-top: 1px solid var(--geo-map-zoom-border);\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a:first-child),\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-touch .leaflet-control-zoom a:first-child),\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-control-zoom .leaflet-control-zoom-in) {\n  border-radius: var(--geo-map-zoom-radius) var(--geo-map-zoom-radius) 0 0 !important;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a:last-child),\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-touch .leaflet-control-zoom a:last-child),\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-control-zoom .leaflet-control-zoom-out) {\n  border-top: 0;\n  border-radius: 0 0 var(--geo-map-zoom-radius) var(--geo-map-zoom-radius) !important;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a:hover) {\n  background: var(--geo-map-zoom-hover-bg);\n  color: var(--geo-map-zoom-hover-fg);\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a:focus),\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a:focus-visible) {\n  position: relative;\n  z-index: 1;\n  outline: 2px solid var(--geo-map-zoom-focus-ring);\n  outline-offset: 1px;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a.leaflet-disabled),\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-control-zoom a.leaflet-disabled:hover) {\n  background: var(--geo-map-zoom-disabled-bg);\n  color: var(--geo-map-zoom-disabled-fg);\n  opacity: 0.55;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-tooltip.geo-map-tooltip) {\n  border: 0;\n  border-radius: var(--geo-map-tooltip-radius);\n  background: var(--geo-map-tooltip-bg);\n  color: var(--geo-map-tooltip-fg);\n  box-shadow: var(--geo-map-tooltip-shadow);\n  font-size: var(--geo-map-tooltip-font-size);\n  font-weight: var(--geo-map-tooltip-font-weight);\n  line-height: var(--geo-map-tooltip-line-height);\n  padding: var(--geo-map-tooltip-padding);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-tooltip-top.geo-map-tooltip::before) {\n  border-top-color: var(--geo-map-tooltip-bg);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-tooltip-bottom.geo-map-tooltip::before) {\n  border-bottom-color: var(--geo-map-tooltip-bg);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-tooltip-left.geo-map-tooltip::before) {\n  border-left-color: var(--geo-map-tooltip-bg);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-tooltip-right.geo-map-tooltip::before) {\n  border-right-color: var(--geo-map-tooltip-bg);\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-popup.geo-map-popup) {\n  margin-bottom: var(--geo-map-popup-margin-bottom);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-content-wrapper) {\n  border: 1px solid var(--geo-map-popup-border);\n  border-radius: var(--geo-map-popup-radius);\n  background: var(--geo-map-popup-bg);\n  color: var(--geo-map-popup-fg);\n  box-shadow: var(--geo-map-popup-shadow);\n  backdrop-filter: blur(var(--geo-map-popup-blur));\n  -webkit-backdrop-filter: blur(var(--geo-map-popup-blur));\n  padding: 0;\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-content) {\n  margin: 0;\n  min-width: 0;\n  width: max-content;\n  max-width: var(--geo-map-popup-max-width);\n  padding: var(--geo-map-popup-content-padding);\n  font-family: var(--geo-map-popup-font-family);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-content p) {\n  margin: 0;\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-tip-container) {\n  display: none;\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-close-button) {\n  color: var(--geo-map-popup-fg);\n  opacity: 0.75;\n  top: 0.25rem;\n  right: 0.25rem;\n  width: 1.5rem;\n  height: 1.5rem;\n  font-size: 1rem;\n  line-height: 1.5rem;\n  border-radius: calc(var(--radius) - 2px);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-close-button:hover) {\n  opacity: 1;\n  background: oklch(from var(--muted) l c h / 0.65);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(\n    .leaflet-popup.geo-map-popup .leaflet-popup-close-button:focus-visible\n  ) {\n  outline: 2px solid var(--ring);\n  outline-offset: 1px;\n}\n"
    },
    {
      "path": "components/tool-ui/geo-map/geo-map.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/geo-map/geo-map.tsx",
      "content": "\"use client\";\n\nimport { memo, useEffect, useState } from \"react\";\nimport { cn } from \"./_adapter\";\nimport { GeoMapEngine } from \"./geo-map-engine\";\nimport styles from \"./geo-map-theme.module.css\";\nimport type { GeoMapProps, GeoMapStyle } from \"./schema\";\n\nconst LIGHT_TILE_URL =\n  \"https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png\";\nconst DARK_TILE_URL =\n  \"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\";\n\nfunction getSystemTheme(): \"light\" | \"dark\" {\n  if (typeof window === \"undefined\") return \"light\";\n  return window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches\n    ? \"dark\"\n    : \"light\";\n}\n\nfunction getDocumentTheme(): \"light\" | \"dark\" | null {\n  if (typeof document === \"undefined\") return null;\n\n  const root = document.documentElement;\n  const dataTheme = root.getAttribute(\"data-theme\")?.toLowerCase();\n  if (dataTheme === \"dark\") return \"dark\";\n  if (dataTheme === \"light\") return \"light\";\n  if (root.classList.contains(\"dark\")) return \"dark\";\n  if (root.classList.contains(\"light\")) return \"light\";\n\n  return null;\n}\n\nfunction useInheritedTheme(): \"light\" | \"dark\" {\n  const [theme, setTheme] = useState<\"light\" | \"dark\">(() => {\n    return getDocumentTheme() ?? getSystemTheme();\n  });\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || typeof document === \"undefined\") {\n      return;\n    }\n\n    const update = () => setTheme(getDocumentTheme() ?? getSystemTheme());\n\n    const mql = window.matchMedia?.(\"(prefers-color-scheme: dark)\");\n    mql?.addEventListener(\"change\", update);\n\n    const observer = new MutationObserver(update);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"data-theme\"],\n    });\n\n    return () => {\n      mql?.removeEventListener(\"change\", update);\n      observer.disconnect();\n    };\n  }, []);\n\n  return theme;\n}\n\nfunction resolveMapAriaLabel(title?: string, description?: string): string {\n  if (title && description) {\n    return `${title}. ${description}`;\n  }\n\n  return title ?? description ?? \"Geographic map\";\n}\n\nexport const GeoMap = memo(function GeoMap({\n  id,\n  role: _role,\n  receipt: _receipt,\n  title,\n  description,\n  markers,\n  routes,\n  clustering,\n  viewport,\n  showZoomControl = true,\n  theme,\n  className,\n  style,\n  tooltipClassName,\n  popupClassName,\n  onMarkerClick,\n  onRouteClick,\n}: GeoMapProps) {\n  const inheritedTheme = useInheritedTheme();\n  const resolvedTheme = theme ?? inheritedTheme;\n  const [isMapReady, setIsMapReady] = useState(false);\n  const tileUrl = resolvedTheme === \"dark\" ? DARK_TILE_URL : LIGHT_TILE_URL;\n  const mapAriaLabel = resolveMapAriaLabel(title, description);\n  const resolvedRootStyle: GeoMapStyle = {\n    \"--geo-map-canvas-bg\":\n      resolvedTheme === \"dark\" ? \"var(--background)\" : \"var(--muted)\",\n    ...style,\n  };\n\n  return (\n    <div\n      className={cn(\"w-full min-w-80\", styles.root, className)}\n      style={resolvedRootStyle}\n      data-slot=\"geo-map\"\n      data-tool-ui-id={id}\n    >\n      <div\n        className=\"bg-muted/20 relative h-[320px] w-full overflow-hidden rounded-lg border\"\n        role=\"region\"\n        aria-label={mapAriaLabel}\n      >\n        <GeoMapEngine\n          id={id}\n          markers={markers}\n          routes={routes}\n          clustering={clustering}\n          viewport={viewport}\n          showZoomControl={showZoomControl}\n          tileUrl={tileUrl}\n          mapAriaLabel={mapAriaLabel}\n          tooltipClassName={tooltipClassName}\n          popupClassName={popupClassName}\n          onMarkerClick={onMarkerClick}\n          onRouteClick={onRouteClick}\n          onReadyChange={setIsMapReady}\n        />\n\n        {(title || description) && (\n          <div\n            className={cn(\n              \"pointer-events-none absolute top-3 left-3 z-[900]\",\n              \"max-w-[min(75%,22rem)] rounded-lg border border-border/70 bg-background/70 px-3 py-2\",\n              \"shadow-sm backdrop-blur-md\",\n            )}\n          >\n            {title && (\n              <p className=\"text-foreground text-sm leading-tight font-semibold\">\n                {title}\n              </p>\n            )}\n            {description && (\n              <p className=\"text-muted-foreground mt-1 text-xs leading-snug\">\n                {description}\n              </p>\n            )}\n          </div>\n        )}\n\n        {!isMapReady && (\n          <div\n            data-slot=\"geo-map-loading\"\n            className=\"bg-muted/30 text-muted-foreground pointer-events-none absolute inset-0 flex items-center justify-center\"\n          >\n            <span data-slot=\"geo-map-loading-label\">Loading map...</span>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n});\n"
    },
    {
      "path": "components/tool-ui/geo-map/index.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/geo-map/index.tsx",
      "content": "export { GeoMap } from \"./geo-map\";\nexport {\n  type GeoMapClustering,\n  type GeoMapFitTarget,\n  type GeoMapMarker,\n  type GeoMapMarkerIcon,\n  type GeoMapStyle,\n  type GeoMapRoute,\n  type GeoMapViewport,\n  type GeoMapProps,\n  type GeoMapClientProps,\n  type SerializableGeoMap,\n} from \"./schema\";\n"
    },
    {
      "path": "components/tool-ui/geo-map/README.md",
      "type": "registry:file",
      "target": "components/tool-ui/geo-map/README.md",
      "content": "# Geo Map\n\nImplementation for the \"geo-map\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/geo-map/index.tsx\n- serializable schema + parse helpers: components/tool-ui/geo-map/schema.ts\n- public facade component: components/tool-ui/geo-map/geo-map.tsx\n- internal Leaflet engine: components/tool-ui/geo-map/geo-map-engine.tsx\n- colocated Leaflet shell theme styles: components/tool-ui/geo-map/geo-map-theme.module.css\n- icon construction helpers: components/tool-ui/geo-map/geo-map-icons.ts\n- popup/tooltip overlay renderer: components/tool-ui/geo-map/geo-map-overlays.tsx\n\n## Companion assets\n\n- Docs page: app/docs/geo-map/content.mdx\n- Preset payload: lib/presets/geo-map.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
    },
    {
      "path": "components/tool-ui/geo-map/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/geo-map/schema.ts",
      "content": "import { z } from \"zod\";\nimport type { CSSProperties } from \"react\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nconst LatitudeSchema = z.number().finite().min(-90).max(90);\nconst LongitudeSchema = z.number().finite().min(-180).max(180);\nconst HttpUrlSchema = z\n  .string()\n  .url()\n  .refine((value) => /^https?:\\/\\//i.test(value), {\n    message: \"Expected an http or https URL.\",\n  });\n\nconst GeoMapMarkerIconDotSchema = z.object({\n  type: z.literal(\"dot\"),\n  color: z.string().optional(),\n  borderColor: z.string().optional(),\n  radius: z.number().min(3).max(16).optional(),\n});\n\nconst GeoMapMarkerIconEmojiSchema = z.object({\n  type: z.literal(\"emoji\"),\n  value: z.string().min(1),\n  size: z.number().min(16).max(40).optional(),\n  bgColor: z.string().optional(),\n  borderColor: z.string().optional(),\n});\n\nconst GeoMapMarkerIconImageSchema = z.object({\n  type: z.literal(\"image\"),\n  url: HttpUrlSchema,\n  width: z.number().min(16).max(64).optional(),\n  height: z.number().min(16).max(64).optional(),\n  borderRadius: z.number().min(0).max(999).optional(),\n  borderColor: z.string().optional(),\n});\n\nexport const GeoMapMarkerIconSchema = z.union([\n  GeoMapMarkerIconDotSchema,\n  GeoMapMarkerIconEmojiSchema,\n  GeoMapMarkerIconImageSchema,\n]);\n\nexport type GeoMapMarkerIcon = z.infer<typeof GeoMapMarkerIconSchema>;\n\nexport const GeoMapMarkerSchema = z.object({\n  id: z.string().min(1).optional(),\n  lat: LatitudeSchema,\n  lng: LongitudeSchema,\n  label: z.string().optional(),\n  description: z.string().optional(),\n  tooltip: z.enum([\"none\", \"hover\", \"always\"]).optional(),\n  icon: GeoMapMarkerIconSchema.optional(),\n});\n\nexport type GeoMapMarker = z.infer<typeof GeoMapMarkerSchema>;\n\nexport const GeoMapRoutePointSchema = z.object({\n  lat: LatitudeSchema,\n  lng: LongitudeSchema,\n});\n\nexport const GeoMapRouteSchema = z.object({\n  id: z.string().min(1).optional(),\n  points: z.array(GeoMapRoutePointSchema).min(2),\n  label: z.string().optional(),\n  description: z.string().optional(),\n  tooltip: z.enum([\"none\", \"hover\", \"always\"]).optional(),\n  color: z.string().optional(),\n  weight: z.number().min(1).max(12).optional(),\n  opacity: z.number().min(0).max(1).optional(),\n  dashArray: z.string().optional(),\n});\n\nexport type GeoMapRoute = z.infer<typeof GeoMapRouteSchema>;\n\nexport const GeoMapClusteringSchema = z.object({\n  enabled: z.boolean().optional(),\n  radius: z.number().min(20).max(120).optional(),\n  maxZoom: z.number().min(1).max(22).optional(),\n  minPoints: z.number().min(2).max(20).optional(),\n});\n\nexport type GeoMapClustering = z.infer<typeof GeoMapClusteringSchema>;\n\nexport const GeoMapFitTargetSchema = z.enum([\"markers\", \"routes\", \"all\"]);\nexport type GeoMapFitTarget = z.infer<typeof GeoMapFitTargetSchema>;\n\nconst GeoMapFitViewportSchema = z.object({\n  mode: z.literal(\"fit\"),\n  padding: z.number().nonnegative().optional(),\n  maxZoom: z.number().min(1).max(22).optional(),\n  target: GeoMapFitTargetSchema.optional(),\n});\n\nconst GeoMapCenterViewportSchema = z.object({\n  mode: z.literal(\"center\"),\n  center: z.object({\n    lat: LatitudeSchema,\n    lng: LongitudeSchema,\n  }),\n  zoom: z.number().min(1).max(22),\n});\n\nexport const GeoMapViewportSchema = z.union([\n  GeoMapFitViewportSchema,\n  GeoMapCenterViewportSchema,\n]);\n\nexport type GeoMapViewport = z.infer<typeof GeoMapViewportSchema>;\n\nexport const GeoMapPropsSchema = z\n  .object({\n    id: ToolUIIdSchema,\n    role: ToolUIRoleSchema.optional(),\n    receipt: ToolUIReceiptSchema.optional(),\n    title: z.string().optional(),\n    description: z.string().optional(),\n    markers: z.array(GeoMapMarkerSchema).min(1),\n    routes: z.array(GeoMapRouteSchema).optional(),\n    clustering: GeoMapClusteringSchema.optional(),\n    viewport: GeoMapViewportSchema.optional(),\n    showZoomControl: z.boolean().optional(),\n    theme: z.enum([\"light\", \"dark\"]).optional(),\n  })\n  .superRefine((value, ctx) => {\n    const seenMarkerIds = new Set<string>();\n\n    value.markers.forEach((marker, index) => {\n      if (!marker.id) {\n        return;\n      }\n\n      if (seenMarkerIds.has(marker.id)) {\n        ctx.addIssue({\n          code: \"custom\",\n          path: [\"markers\", index, \"id\"],\n          message: `Duplicate marker id \"${marker.id}\".`,\n        });\n        return;\n      }\n\n      seenMarkerIds.add(marker.id);\n    });\n\n    const seenRouteIds = new Set<string>();\n    value.routes?.forEach((route, index) => {\n      if (!route.id) {\n        return;\n      }\n\n      if (seenRouteIds.has(route.id)) {\n        ctx.addIssue({\n          code: \"custom\",\n          path: [\"routes\", index, \"id\"],\n          message: `Duplicate route id \"${route.id}\".`,\n        });\n        return;\n      }\n\n      seenRouteIds.add(route.id);\n    });\n  });\n\nexport type GeoMapStyle = CSSProperties &\n  Partial<Record<`--${string}`, string | number>>;\n\nexport type GeoMapClientProps = {\n  className?: string;\n  style?: GeoMapStyle;\n  tooltipClassName?: string;\n  popupClassName?: string;\n  onMarkerClick?: (marker: GeoMapMarker) => void;\n  onRouteClick?: (route: GeoMapRoute) => void;\n};\n\nexport type GeoMapProps = z.infer<typeof GeoMapPropsSchema> & GeoMapClientProps;\n\nexport const SerializableGeoMapSchema = GeoMapPropsSchema;\n\nexport type SerializableGeoMap = z.infer<typeof SerializableGeoMapSchema>;\n\nconst SerializableGeoMapSchemaContract = defineToolUiContract(\n  \"GeoMap\",\n  SerializableGeoMapSchema,\n);\n\nexport const parseSerializableGeoMap: (input: unknown) => SerializableGeoMap =\n  SerializableGeoMapSchemaContract.parse;\n\nexport const safeParseSerializableGeoMap: (\n  input: unknown,\n) => SerializableGeoMap | null = SerializableGeoMapSchemaContract.safeParse;\n"
    },
    {
      "path": "components/tool-ui/shared/contract.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/contract.ts",
      "content": "import { z } from \"zod\";\nimport { parseWithSchema, safeParseWithSchema } from \"./parse\";\n\nexport interface ToolUiContract<T> {\n  schema: z.ZodType<T>;\n  parse: (input: unknown) => T;\n  safeParse: (input: unknown) => T | null;\n}\n\nexport function defineToolUiContract<T>(\n  componentName: string,\n  schema: z.ZodType<T>,\n): ToolUiContract<T> {\n  return {\n    schema,\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\n  };\n}\n"
    },
    {
      "path": "components/tool-ui/shared/parse.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/parse.ts",
      "content": "import { z } from \"zod\";\n\nfunction formatZodPath(path: Array<string | number | symbol>): string {\n  if (path.length === 0) return \"root\";\n  return path\n    .map((segment) =>\n      typeof segment === \"number\" ? `[${segment}]` : String(segment),\n    )\n    .join(\".\");\n}\n\n/**\n * Format Zod errors into a compact `path: message` string.\n */\nexport function formatZodError(error: z.ZodError): string {\n  const parts = error.issues.map((issue) => {\n    const path = formatZodPath(issue.path);\n    return `${path}: ${issue.message}`;\n  });\n\n  return Array.from(new Set(parts)).join(\"; \");\n}\n\n/**\n * Parse unknown input and throw a readable error.\n */\nexport function parseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n  name: string,\n): T {\n  const res = schema.safeParse(input);\n  if (!res.success) {\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\n  }\n  return res.data;\n}\n\n/**\n * Parse unknown input, returning `null` instead of throwing on failure.\n *\n * Use this in assistant-ui `render` functions where `args` stream in\n * incrementally and may be incomplete until the tool call finishes.\n */\nexport function safeParseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n): T | null {\n  const res = schema.safeParse(input);\n  return res.success ? res.data : null;\n}\n"
    },
    {
      "path": "components/tool-ui/shared/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/schema.ts",
      "content": "import { z } from \"zod\";\nimport type { ReactNode } from \"react\";\n\n/**\n * Tool UI conventions:\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\n * - Schema: `SerializableXSchema`\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\n * - Root attrs: `data-tool-ui-id` + `data-slot`\n */\n\n/**\n * Schema for tool UI identity.\n *\n * Every tool UI should have a unique identifier that:\n * - Is stable across re-renders\n * - Is meaningful (not auto-generated)\n * - Is unique within the conversation\n *\n * Format recommendation: `{component-type}-{semantic-identifier}`\n * Examples: \"data-table-expenses-q3\", \"option-list-deploy-target\"\n */\nexport const ToolUIIdSchema = z.string().min(1);\n\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\n\n/**\n * Primary role of a Tool UI surface in a chat context.\n */\nexport const ToolUIRoleSchema = z.enum([\n  \"information\",\n  \"decision\",\n  \"control\",\n  \"state\",\n  \"composite\",\n]);\n\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\n\nexport const ToolUIReceiptOutcomeSchema = z.enum([\n  \"success\",\n  \"partial\",\n  \"failed\",\n  \"cancelled\",\n]);\n\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\n\n/**\n * Optional receipt metadata: a durable summary of an outcome.\n */\nexport const ToolUIReceiptSchema = z.object({\n  outcome: ToolUIReceiptOutcomeSchema,\n  summary: z.string().min(1),\n  identifiers: z.record(z.string(), z.string()).optional(),\n  at: z.string().datetime(),\n});\n\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\n\n/**\n * Base schema for Tool UI payloads (id + optional role/receipt).\n */\nexport const ToolUISurfaceSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n});\n\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\n\nexport const ActionSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  /**\n   * Canonical narration the assistant can use after this action is taken.\n   *\n   * Example: \"I exported the table as CSV.\" / \"I opened the link in a new tab.\"\n   */\n  sentence: z.string().optional(),\n  confirmLabel: z.string().optional(),\n  variant: z\n    .enum([\"default\", \"destructive\", \"secondary\", \"ghost\", \"outline\"])\n    .optional(),\n  icon: z.custom<ReactNode>().optional(),\n  loading: z.boolean().optional(),\n  disabled: z.boolean().optional(),\n  shortcut: z.string().optional(),\n});\n\nexport type Action = z.infer<typeof ActionSchema>;\nexport type LocalAction = Action;\nexport type DecisionAction = Action;\n\nexport const DecisionResultSchema = z.object({\n  kind: z.literal(\"decision\"),\n  version: z.literal(1),\n  decisionId: z.string().min(1),\n  actionId: z.string().min(1),\n  actionLabel: z.string().min(1),\n  at: z.string().datetime(),\n  payload: z.record(z.string(), z.unknown()).optional(),\n});\n\nexport type DecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n> = Omit<z.infer<typeof DecisionResultSchema>, \"payload\"> & {\n  payload?: TPayload;\n};\n\nexport function createDecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n>(args: {\n  decisionId: string;\n  action: { id: string; label: string };\n  payload?: TPayload;\n}): DecisionResult<TPayload> {\n  return {\n    kind: \"decision\",\n    version: 1,\n    decisionId: args.decisionId,\n    actionId: args.action.id,\n    actionLabel: args.action.label,\n    at: new Date().toISOString(),\n    payload: args.payload,\n  };\n}\n\nexport const ActionButtonsPropsSchema = z.object({\n  actions: z.array(ActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n  className: z.string().optional(),\n});\n\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\n  actions: z.array(SerializableActionSchema),\n}).omit({ className: true });\n\nexport interface ActionsConfig {\n  items: Action[];\n  align?: \"left\" | \"center\" | \"right\";\n  confirmTimeout?: number;\n}\n\nexport const SerializableActionsConfigSchema = z.object({\n  items: z.array(SerializableActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n});\n\nexport type SerializableActionsConfig = z.infer<\n  typeof SerializableActionsConfigSchema\n>;\n\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\n"
    }
  ]
}
