{
  "name": "map-utils",
  "type": "registry:lib",
  "description": "MapLibre and Turf geospatial helpers",
  "dependencies": [
    "maplibre-gl",
    "@turf/turf"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/bases/default/lib/map-utils.ts",
      "type": "registry:lib",
      "content": "import * as turf from \"@turf/turf\";\nimport type { CSSProperties } from \"react\";\nimport type { Feature, FeatureCollection, LineString, Point } from \"geojson\";\nimport type { Map } from \"maplibre-gl\";\nimport maplibregl from \"maplibre-gl\";\n\nexport type LngLat = [number, number];\n\n/** Dark basemap aligned with RemotionUI demo palette */\nexport const DEFAULT_MAP_STYLE =\n  \"https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json\";\n\nexport const MAP_THEME = {\n  background: \"#080810\",\n  route: \"#2dd4bf\",\n  routeGlow: \"rgba(45, 212, 191, 0.42)\",\n  routeHead: \"#f8fafc\",\n  marker: \"#e8b86d\",\n  markerGlow: \"rgba(232, 184, 109, 0.38)\",\n  label: \"#f8fafc\",\n  labelHalo: \"#080810\",\n  vignette: \"rgba(8, 8, 16, 0.55)\",\n} as const;\n\nexport const DEMO_ROUTE: { from: LngLat; to: LngLat } = {\n  from: [8.5417, 47.3769],\n  to: [-74.006, 40.7128],\n};\n\nexport const DEMO_MARKERS: Array<{ position: LngLat; name: string }> = [\n  { position: DEMO_ROUTE.from, name: \"Zurich\" },\n  { position: [-0.1276, 51.5074], name: \"London\" },\n  { position: DEMO_ROUTE.to, name: \"New York\" },\n];\n\nexport function clampProgress(progress: number) {\n  return Math.min(1, Math.max(0, progress));\n}\n\nexport function distanceAlong(totalDistance: number, progress: number) {\n  return Math.max(0.001, totalDistance * clampProgress(progress));\n}\n\nexport function greatCircleLine(from: LngLat, to: LngLat) {\n  const route = turf.greatCircle(from, to, { npoints: 120 });\n\n  if (route.geometry.type === \"LineString\") {\n    return turf.lineString(route.geometry.coordinates);\n  }\n\n  const longestSegment = route.geometry.coordinates.reduce((longest, segment) =>\n    segment.length > longest.length ? segment : longest,\n  );\n\n  return turf.lineString(longestSegment);\n}\n\nexport function sliceRouteByProgress(\n  line: Feature<LineString>,\n  progress: number,\n) {\n  const totalDistance = turf.length(line);\n  return turf.lineSliceAlong(\n    line,\n    0,\n    distanceAlong(totalDistance, progress),\n  );\n}\n\nexport function pointAlongRoute(line: Feature<LineString>, progress: number) {\n  const totalDistance = turf.length(line);\n  return turf.along(line, distanceAlong(totalDistance, progress)).geometry\n    .coordinates as LngLat;\n}\n\nexport function createMarkerCollection(\n  markers: Array<{ position: LngLat; name: string }>,\n): FeatureCollection<Point> {\n  return turf.featureCollection(\n    markers.map((marker) =>\n      turf.point(marker.position, { name: marker.name }),\n    ),\n  );\n}\n\nexport function getCameraOptions(\n  map: Map,\n  targetRoute: Feature<LineString>,\n  cameraRoute: Feature<LineString>,\n  progress: number,\n  cameraAltitudeMeters: number,\n  cameraLatitudeOffset = 0,\n) {\n  const target = pointAlongRoute(targetRoute, progress);\n  const camera = pointAlongRoute(cameraRoute, progress);\n\n  return map.calculateCameraOptionsFromTo(\n    new maplibregl.LngLat(camera[0], camera[1] - cameraLatitudeOffset),\n    cameraAltitudeMeters,\n    new maplibregl.LngLat(target[0], target[1]),\n  );\n}\n\nexport type MapInitOptions = {\n  center: LngLat;\n  zoom?: number;\n  style?: string;\n};\n\nexport function createMapInstance(\n  container: HTMLDivElement,\n  width: number,\n  height: number,\n  { center, zoom = 7, style = DEFAULT_MAP_STYLE }: MapInitOptions,\n) {\n  return new maplibregl.Map({\n    container,\n    style,\n    center,\n    zoom,\n    interactive: false,\n    attributionControl: false,\n    fadeDuration: 0,\n    canvasContextAttributes: {\n      preserveDrawingBuffer: true,\n    },\n  });\n}\n\n/** MapLibre throws when style is torn down — guard all layer/source access. */\nexport function isMapStyleReady(map: Map | null | undefined): map is Map {\n  if (!map) {\n    return false;\n  }\n\n  try {\n    return Boolean(map.getStyle());\n  } catch {\n    return false;\n  }\n}\n\nexport function hasMapLayer(map: Map, layerId: string): boolean {\n  if (!isMapStyleReady(map)) {\n    return false;\n  }\n\n  try {\n    return Boolean(map.getLayer(layerId));\n  } catch {\n    return false;\n  }\n}\n\nexport function hasMapSource(map: Map, sourceId: string): boolean {\n  if (!isMapStyleReady(map)) {\n    return false;\n  }\n\n  try {\n    return Boolean(map.getSource(sourceId));\n  } catch {\n    return false;\n  }\n}\n\nexport function removeMapLayer(map: Map, layerId: string) {\n  if (hasMapLayer(map, layerId)) {\n    map.removeLayer(layerId);\n  }\n}\n\nexport function removeMapSource(map: Map, sourceId: string) {\n  if (hasMapSource(map, sourceId)) {\n    map.removeSource(sourceId);\n  }\n}\n\nexport const mapVignetteStyle: CSSProperties = {\n  position: \"absolute\",\n  inset: 0,\n  pointerEvents: \"none\",\n  background: [\n    `radial-gradient(ellipse 88% 72% at 50% 48%, transparent 42%, ${MAP_THEME.vignette} 100%)`,\n    \"linear-gradient(to bottom, rgba(8,8,16,0.42) 0%, transparent 18%, transparent 78%, rgba(8,8,16,0.62) 100%)\",\n  ].join(\", \"),\n};\n"
    }
  ]
}