{
  "name": "map-flight",
  "type": "registry:block",
  "description": "Animated map flyover with route reveal and markers",
  "dependencies": [
    "remotion",
    "maplibre-gl",
    "@turf/turf"
  ],
  "registryDependencies": [
    "map-route",
    "map-markers",
    "map-utils",
    "layout",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/scenes/map-flight/index.tsx",
      "type": "registry:block",
      "content": "import { loadFont } from \"@remotion/google-fonts/Inter\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  interpolate,\n  useCurrentFrame,\n  useDelayRender,\n  useVideoConfig,\n} from \"remotion\";\nimport type { Map } from \"maplibre-gl\";\nimport \"maplibre-gl/dist/maplibre-gl.css\";\nimport { MapMarkers } from \"@/remotion/primitives/map-markers\";\nimport { MapRoute } from \"@/remotion/primitives/map-route\";\nimport {\n  createMapInstance,\n  createMarkerCollection,\n  DEMO_ROUTE,\n  getCameraOptions,\n  greatCircleLine,\n  mapVignetteStyle,\n  MAP_THEME,\n  type LngLat,\n} from \"@/remotion/lib/map-utils\";\nimport { getSafeAreaPadding, scaleFont } from \"@/remotion/lib/layout\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"600\", \"700\"],\n  subsets: [\"latin\"],\n});\n\nexport type MapFlightProps = {\n  from?: LngLat;\n  to?: LngLat;\n  fromLabel?: string;\n  toLabel?: string;\n  backgroundColor?: string;\n};\n\nexport const MapFlight: React.FC<MapFlightProps> = ({\n  from = DEMO_ROUTE.from,\n  to = DEMO_ROUTE.to,\n  fromLabel,\n  toLabel,\n  backgroundColor = MAP_THEME.background,\n}) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const frame = useCurrentFrame();\n  const { delayRender, continueRender } = useDelayRender();\n  const { durationInFrames, width, height } = useVideoConfig();\n  const [map, setMap] = useState<Map | null>(null);\n  const [loadingHandle] = useState(() => delayRender(\"Loading MapLibre map\"));\n\n  const targetRoute = useMemo(() => greatCircleLine(from, to), [from, to]);\n  const cameraRoute = useMemo(() => greatCircleLine(from, to), [from, to]);\n  const markers = useMemo(\n    () =>\n      createMarkerCollection([\n        { position: from, name: \"\" },\n        { position: to, name: \"\" },\n      ]),\n    [from, to],\n  );\n\n  useEffect(() => {\n    if (!containerRef.current) {\n      return;\n    }\n\n    const mapInstance = createMapInstance(\n      containerRef.current,\n      width,\n      height,\n      { center: from, zoom: 5 },\n    );\n\n    mapInstance.on(\"load\", () => {\n      mapInstance.jumpTo({ center: from, zoom: 5 });\n      mapInstance.once(\"idle\", () => {\n        setMap(mapInstance);\n        continueRender(loadingHandle);\n      });\n    });\n  }, [continueRender, from, height, loadingHandle, width]);\n\n  useEffect(() => {\n    if (!map) {\n      return;\n    }\n\n    const handle = delayRender(\"Rendering MapLibre frame\");\n    const timelineProgress = interpolate(\n      frame,\n      [0, durationInFrames - 1],\n      [0, 1],\n      {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n      },\n    );\n    const travelProgress = interpolate(timelineProgress, [0.2, 0.82], [0, 1], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: EASING.editorial,\n    });\n    const cameraAltitudeMeters = interpolate(\n      timelineProgress,\n      [0, 0.28, 0.74, 1],\n      [180000, 2200000, 2200000, 180000],\n      {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n        easing: EASING.editorial,\n      },\n    );\n    const cameraLatitudeOffset = interpolate(\n      timelineProgress,\n      [0, 0.28, 0.74, 1],\n      [1.1, 8, 8, 1.1],\n      {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n        easing: EASING.editorial,\n      },\n    );\n\n    map.jumpTo(\n      getCameraOptions(\n        map,\n        targetRoute,\n        cameraRoute,\n        travelProgress,\n        cameraAltitudeMeters,\n        cameraLatitudeOffset,\n      ),\n    );\n\n    map.once(\"idle\", () => continueRender(handle));\n    map.triggerRepaint();\n  }, [\n    cameraRoute,\n    continueRender,\n    delayRender,\n    durationInFrames,\n    frame,\n    map,\n    targetRoute,\n  ]);\n\n  const timelineProgress = interpolate(\n    frame,\n    [0, durationInFrames - 1],\n    [0, 1],\n    {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n    },\n  );\n  const routeProgress = interpolate(timelineProgress, [0.2, 0.82], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.editorial,\n  });\n  const safe = getSafeAreaPadding({ width, height });\n  const showLabels = Boolean(fromLabel || toLabel);\n  const fromReveal = interpolate(timelineProgress, [0, 0.14], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const toReveal = interpolate(timelineProgress, [0.78, 0.94], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  return (\n    <AbsoluteFill style={{ backgroundColor }}>\n      <div\n        ref={containerRef}\n        style={{ width, height, position: \"absolute\" }}\n      />\n      <MapRoute map={map} route={targetRoute} progress={routeProgress} />\n      <MapMarkers map={map} markers={markers} revealProgress={1} />\n      <div style={mapVignetteStyle} />\n      {showLabels ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: safe.paddingLeft,\n            right: safe.paddingRight,\n            bottom: safe.paddingBottom,\n            display: \"flex\",\n            justifyContent: \"space-between\",\n            gap: 16,\n            fontFamily,\n            pointerEvents: \"none\",\n          }}\n        >\n          {fromLabel ? (\n            <EndpointLabel label={fromLabel} reveal={fromReveal} width={width} />\n          ) : null}\n          {toLabel ? (\n            <EndpointLabel label={toLabel} reveal={toReveal} width={width} />\n          ) : null}\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n};\n\nconst EndpointLabel: React.FC<{\n  label: string;\n  reveal: number;\n  width: number;\n}> = ({ label, reveal, width }) => (\n  <div\n    style={{\n      opacity: reveal,\n      transform: `translateY(${(1 - reveal) * 12}px)`,\n      padding: `${scaleFont(10, width)}px ${scaleFont(18, width)}px`,\n      borderRadius: 12,\n      background: \"rgba(8, 8, 16, 0.82)\",\n      border: \"1px solid rgba(255, 255, 255, 0.1)\",\n      color: MAP_THEME.label,\n      fontSize: scaleFont(26, width),\n      fontWeight: 700,\n      boxShadow: \"0 12px 40px rgba(0,0,0,0.35)\",\n    }}\n  >\n    <span\n      style={{\n        display: \"inline-block\",\n        borderBottom: `2px solid ${MAP_THEME.marker}`,\n        paddingBottom: 2,\n      }}\n    >\n      {label}\n    </span>\n  </div>\n);\n"
    }
  ],
  "atlas": {
    "lane": "spatial",
    "drive": "spatial",
    "tier": "advanced"
  }
}