{
  "name": "map-markers",
  "type": "registry:ui",
  "description": "GeoJSON circle and label markers on a map",
  "dependencies": [
    "remotion",
    "maplibre-gl"
  ],
  "registryDependencies": [
    "map-utils",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/map-markers.tsx",
      "type": "registry:ui",
      "content": "import type { FeatureCollection, Point } from \"geojson\";\nimport { useEffect } from \"react\";\nimport { interpolate, useCurrentFrame } from \"remotion\";\nimport type { GeoJSONSource, Map } from \"maplibre-gl\";\nimport {\n  clampProgress,\n  hasMapLayer,\n  hasMapSource,\n  isMapStyleReady,\n  MAP_THEME,\n  removeMapLayer,\n  removeMapSource,\n} from \"@/remotion/lib/map-utils\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\n\nexport type MapMarkersProps = {\n  map: Map | null;\n  markers: FeatureCollection<Point>;\n  dotColor?: string;\n  glowColor?: string;\n  dotRadius?: number;\n  labelSize?: number;\n  sourceId?: string;\n  /** Staggered entrance 0–1; defaults to frame-driven reveal */\n  revealProgress?: number;\n};\n\nconst applyMarkerReveal = ({\n  map,\n  sourceId,\n  dotRadius,\n  revealProgress,\n}: {\n  map: Map;\n  sourceId: string;\n  dotRadius: number;\n  revealProgress: number;\n}) => {\n  if (!isMapStyleReady(map)) {\n    return;\n  }\n\n  const labelOpacity = revealProgress;\n  const dotScale = 0.72 + revealProgress * 0.28;\n  const labelsId = `${sourceId}-labels`;\n  const dotsId = `${sourceId}-dots`;\n  const glowId = `${sourceId}-glow`;\n\n  if (hasMapLayer(map, labelsId)) {\n    map.setPaintProperty(labelsId, \"text-opacity\", labelOpacity);\n  }\n  if (hasMapLayer(map, dotsId)) {\n    map.setPaintProperty(dotsId, \"circle-radius\", dotRadius * dotScale);\n    map.setPaintProperty(dotsId, \"circle-opacity\", labelOpacity);\n  }\n  if (hasMapLayer(map, glowId)) {\n    map.setPaintProperty(glowId, \"circle-radius\", dotRadius * 2.4 * dotScale);\n    map.setPaintProperty(glowId, \"circle-opacity\", 0.55 * labelOpacity);\n  }\n\n  map.triggerRepaint();\n};\n\nexport const MapMarkers: React.FC<MapMarkersProps> = ({\n  map,\n  markers,\n  dotColor = MAP_THEME.marker,\n  glowColor = MAP_THEME.markerGlow,\n  dotRadius = 11,\n  labelSize = 28,\n  sourceId = \"city-markers\",\n  revealProgress: revealProgressProp,\n}) => {\n  const frame = useCurrentFrame();\n  const revealProgress =\n    revealProgressProp ??\n    clampProgress(\n      interpolate(frame, [6, 34], [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    if (!hasMapSource(map, sourceId)) {\n      map.addSource(sourceId, {\n        type: \"geojson\",\n        data: markers,\n      });\n\n      map.addLayer({\n        id: `${sourceId}-glow`,\n        type: \"circle\",\n        source: sourceId,\n        paint: {\n          \"circle-color\": glowColor,\n          \"circle-radius\": dotRadius * 2.4,\n          \"circle-opacity\": 0.55,\n          \"circle-blur\": 0.35,\n        },\n      });\n\n      map.addLayer({\n        id: `${sourceId}-dots`,\n        type: \"circle\",\n        source: sourceId,\n        paint: {\n          \"circle-color\": dotColor,\n          \"circle-radius\": dotRadius,\n          \"circle-stroke-color\": MAP_THEME.label,\n          \"circle-stroke-width\": 3,\n        },\n      });\n\n      map.addLayer({\n        id: `${sourceId}-labels`,\n        type: \"symbol\",\n        source: sourceId,\n        layout: {\n          \"text-allow-overlap\": true,\n          \"text-anchor\": \"top\",\n          \"text-field\": [\"get\", \"name\"],\n          \"text-offset\": [0, 1],\n          \"text-size\": labelSize,\n        },\n        paint: {\n          \"text-color\": MAP_THEME.label,\n          \"text-halo-color\": MAP_THEME.labelHalo,\n          \"text-halo-width\": 2.5,\n        },\n      });\n    } else {\n      const source = map.getSource(sourceId) as GeoJSONSource | undefined;\n      source?.setData(markers);\n\n      if (hasMapLayer(map, `${sourceId}-dots`)) {\n        map.setPaintProperty(`${sourceId}-dots`, \"circle-color\", dotColor);\n        map.setPaintProperty(`${sourceId}-dots`, \"circle-radius\", dotRadius);\n      }\n      if (hasMapLayer(map, `${sourceId}-glow`)) {\n        map.setPaintProperty(`${sourceId}-glow`, \"circle-color\", glowColor);\n        map.setPaintProperty(\n          `${sourceId}-glow`,\n          \"circle-radius\",\n          dotRadius * 2.4,\n        );\n      }\n      if (hasMapLayer(map, `${sourceId}-labels`)) {\n        map.setLayoutProperty(`${sourceId}-labels`, \"text-size\", labelSize);\n      }\n    }\n\n    return () => {\n      if (!isMapStyleReady(map)) {\n        return;\n      }\n\n      removeMapLayer(map, `${sourceId}-labels`);\n      removeMapLayer(map, `${sourceId}-dots`);\n      removeMapLayer(map, `${sourceId}-glow`);\n      removeMapSource(map, sourceId);\n    };\n  }, [dotColor, dotRadius, glowColor, labelSize, map, markers, sourceId]);\n\n  useEffect(() => {\n    if (!isMapStyleReady(map)) {\n      return;\n    }\n\n    applyMarkerReveal({ map, sourceId, dotRadius, revealProgress });\n  }, [dotRadius, map, revealProgress, sourceId]);\n\n  return null;\n};\n"
    }
  ],
  "atlas": {
    "lane": "spatial",
    "drive": "spatial",
    "tier": "advanced"
  }
}