{
  "name": "map-route",
  "type": "registry:ui",
  "description": "Animated GeoJSON route line on a map",
  "dependencies": [
    "remotion",
    "maplibre-gl",
    "@turf/turf"
  ],
  "registryDependencies": [
    "map-utils",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/map-route.tsx",
      "type": "registry:ui",
      "content": "import type { Feature, LineString } from \"geojson\";\nimport { useEffect } from \"react\";\nimport {\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useDelayRender,\n  useVideoConfig,\n} from \"remotion\";\nimport type { GeoJSONSource, Map } from \"maplibre-gl\";\nimport * as turf from \"@turf/turf\";\nimport {\n  clampProgress,\n  hasMapLayer,\n  hasMapSource,\n  isMapStyleReady,\n  MAP_THEME,\n  pointAlongRoute,\n  removeMapLayer,\n  removeMapSource,\n  sliceRouteByProgress,\n} from \"@/remotion/lib/map-utils\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\n\nexport type MapRouteProps = {\n  map: Map | null;\n  route: Feature<LineString>;\n  progress?: number;\n  lineColor?: string;\n  glowColor?: string;\n  lineWidth?: number;\n  sourceId?: string;\n  layerId?: string;\n  showHead?: boolean;\n};\n\nexport const MapRoute: React.FC<MapRouteProps> = ({\n  map,\n  route,\n  progress: progressProp,\n  lineColor = MAP_THEME.route,\n  glowColor = MAP_THEME.routeGlow,\n  lineWidth = 5,\n  sourceId = \"trace\",\n  layerId = \"trace-line\",\n  showHead = true,\n}) => {\n  const frame = useCurrentFrame();\n  const { durationInFrames } = useVideoConfig();\n  const { delayRender, continueRender } = useDelayRender();\n\n  const progress =\n    progressProp ??\n    clampProgress(\n      interpolate(frame, [8, durationInFrames - 12], [0, 1], {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n        easing: EASING.enter,\n      }),\n    );\n\n  useEffect(() => {\n    if (!isMapStyleReady(map)) {\n      return;\n    }\n\n    const partialRoute = sliceRouteByProgress(route, 0);\n    const headSourceId = `${sourceId}-head`;\n\n    if (!hasMapSource(map, sourceId)) {\n      map.addSource(sourceId, {\n        type: \"geojson\",\n        data: partialRoute,\n      });\n\n      map.addLayer({\n        id: `${layerId}-glow`,\n        type: \"line\",\n        source: sourceId,\n        layout: {\n          \"line-cap\": \"round\",\n          \"line-join\": \"round\",\n        },\n        paint: {\n          \"line-color\": glowColor,\n          \"line-width\": lineWidth * 2.8,\n          \"line-blur\": 1.2,\n          \"line-opacity\": 0.72,\n        },\n      });\n\n      map.addLayer({\n        id: layerId,\n        type: \"line\",\n        source: sourceId,\n        layout: {\n          \"line-cap\": \"round\",\n          \"line-join\": \"round\",\n        },\n        paint: {\n          \"line-color\": lineColor,\n          \"line-width\": lineWidth,\n          \"line-opacity\": 0.96,\n        },\n      });\n\n      if (showHead) {\n        map.addSource(headSourceId, {\n          type: \"geojson\",\n          data: turf.featureCollection([\n            turf.point(pointAlongRoute(route, 0)),\n          ]),\n        });\n\n        map.addLayer({\n          id: `${layerId}-head-glow`,\n          type: \"circle\",\n          source: headSourceId,\n          paint: {\n            \"circle-color\": glowColor,\n            \"circle-radius\": 14,\n            \"circle-blur\": 0.4,\n            \"circle-opacity\": 0.7,\n          },\n        });\n\n        map.addLayer({\n          id: `${layerId}-head`,\n          type: \"circle\",\n          source: headSourceId,\n          paint: {\n            \"circle-color\": MAP_THEME.routeHead,\n            \"circle-radius\": 6,\n            \"circle-stroke-color\": lineColor,\n            \"circle-stroke-width\": 3,\n          },\n        });\n      }\n    } else {\n      if (hasMapLayer(map, layerId)) {\n        map.setPaintProperty(layerId, \"line-color\", lineColor);\n        map.setPaintProperty(layerId, \"line-width\", lineWidth);\n      }\n      if (hasMapLayer(map, `${layerId}-glow`)) {\n        map.setPaintProperty(`${layerId}-glow`, \"line-color\", glowColor);\n        map.setPaintProperty(`${layerId}-glow`, \"line-width\", lineWidth * 2.8);\n      }\n    }\n\n    return () => {\n      if (!isMapStyleReady(map)) {\n        return;\n      }\n\n      removeMapLayer(map, `${layerId}-head`);\n      removeMapLayer(map, `${layerId}-head-glow`);\n      removeMapLayer(map, layerId);\n      removeMapLayer(map, `${layerId}-glow`);\n      removeMapSource(map, headSourceId);\n      removeMapSource(map, sourceId);\n    };\n  }, [\n    glowColor,\n    layerId,\n    lineColor,\n    lineWidth,\n    map,\n    route,\n    showHead,\n    sourceId,\n  ]);\n\n  useEffect(() => {\n    if (!isMapStyleReady(map)) {\n      return;\n    }\n\n    const handle = delayRender(\"Rendering map route\");\n    const partialRoute = sliceRouteByProgress(route, progress);\n    const source = map.getSource(sourceId) as GeoJSONSource | undefined;\n    source?.setData(partialRoute);\n\n    if (showHead && progress > 0.01) {\n      const head = pointAlongRoute(route, progress);\n      const headSource = map.getSource(`${sourceId}-head`) as\n        | GeoJSONSource\n        | undefined;\n      headSource?.setData(turf.featureCollection([turf.point(head)]));\n      if (hasMapLayer(map, `${layerId}-head`)) {\n        map.setPaintProperty(`${layerId}-head`, \"circle-opacity\", 1);\n      }\n      if (hasMapLayer(map, `${layerId}-head-glow`)) {\n        map.setPaintProperty(`${layerId}-head-glow`, \"circle-opacity\", 0.7);\n      }\n    } else if (showHead) {\n      if (hasMapLayer(map, `${layerId}-head`)) {\n        map.setPaintProperty(`${layerId}-head`, \"circle-opacity\", 0);\n      }\n      if (hasMapLayer(map, `${layerId}-head-glow`)) {\n        map.setPaintProperty(`${layerId}-head-glow`, \"circle-opacity\", 0);\n      }\n    }\n\n    const lineOpacity = interpolate(progress, [0, 0.04], [0, 1], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: Easing.out(Easing.cubic),\n    });\n    if (hasMapLayer(map, layerId)) {\n      map.setPaintProperty(layerId, \"line-opacity\", lineOpacity * 0.96);\n    }\n    if (hasMapLayer(map, `${layerId}-glow`)) {\n      map.setPaintProperty(`${layerId}-glow`, \"line-opacity\", lineOpacity * 0.72);\n    }\n\n    map.once(\"idle\", () => continueRender(handle));\n    map.triggerRepaint();\n  }, [\n    continueRender,\n    delayRender,\n    layerId,\n    map,\n    progress,\n    route,\n    showHead,\n    sourceId,\n  ]);\n\n  return null;\n};\n"
    }
  ],
  "atlas": {
    "lane": "spatial",
    "drive": "spatial",
    "tier": "advanced"
  }
}