{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "circle",
  "title": "Map Circle",
  "description": "Draggable geographic circle with customizable fill and stroke.",
  "dependencies": ["mapbox-gl"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/circle.tsx",
      "content": "\"use client\"\n\nimport type mapboxgl from \"mapbox-gl\"\n\nimport { useEffect, useId, useRef } from \"react\"\nimport { useMap } from \"./hooks\"\nimport type { MapCoordinates, MapPath } from \"./types\"\n\ntype MapCircleProps = {\n  center: MapCoordinates\n  radius: number\n  fillColor?: string\n  fillOpacity?: number\n  strokeColor?: string\n  strokeWidth?: number\n  strokeOpacity?: number\n  dashArray?: [number, number]\n  segments?: number\n  draggable?: boolean\n  cursor?: string\n  onDragStart?: (center: MapCoordinates) => void\n  onDrag?: (center: MapCoordinates) => void\n  onDragEnd?: (center: MapCoordinates) => void\n}\n\nconst DEFAULT_FILL_COLOR = \"#3b82f6\"\nconst DEFAULT_FILL_OPACITY = 0\nconst DEFAULT_STROKE_COLOR = \"#3b82f6\"\nconst DEFAULT_STROKE_WIDTH = 2\nconst DEFAULT_STROKE_OPACITY = 1\nconst DEFAULT_SEGMENTS = 64\nconst EARTH_RADIUS_METERS = 6371008.8\n\nconst generateCircleCoordinates = (center: MapCoordinates, radiusMeters: number, segments: number): MapPath => {\n  const [lng, lat] = center\n  const coordinates: MapPath = []\n\n  const latRadians = (lat * Math.PI) / 180\n  const lngRadians = (lng * Math.PI) / 180\n  const angularDistance = radiusMeters / EARTH_RADIUS_METERS\n\n  for (let i = 0; i <= segments; i++) {\n    const bearing = (2 * Math.PI * i) / segments\n\n    const pointLat = Math.asin(\n      Math.sin(latRadians) * Math.cos(angularDistance) +\n        Math.cos(latRadians) * Math.sin(angularDistance) * Math.cos(bearing)\n    )\n\n    const pointLng =\n      lngRadians +\n      Math.atan2(\n        Math.sin(bearing) * Math.sin(angularDistance) * Math.cos(latRadians),\n        Math.cos(angularDistance) - Math.sin(latRadians) * Math.sin(pointLat)\n      )\n\n    coordinates.push([(pointLng * 180) / Math.PI, (pointLat * 180) / Math.PI])\n  }\n\n  return coordinates\n}\n\nexport const MapCircle = ({\n  center,\n  radius,\n  fillColor = DEFAULT_FILL_COLOR,\n  fillOpacity = DEFAULT_FILL_OPACITY,\n  strokeColor = DEFAULT_STROKE_COLOR,\n  strokeWidth = DEFAULT_STROKE_WIDTH,\n  strokeOpacity = DEFAULT_STROKE_OPACITY,\n  dashArray,\n  segments = DEFAULT_SEGMENTS,\n  draggable = false,\n  cursor,\n  onDragStart,\n  onDrag,\n  onDragEnd,\n}: MapCircleProps) => {\n  const { map, isLoaded } = useMap()\n  const id = useId()\n  const sourceId = `circle-source-${id}`\n  const fillLayerId = `circle-fill-layer-${id}`\n  const strokeLayerId = `circle-stroke-layer-${id}`\n  const initializedRef = useRef(false)\n  const isDraggingRef = useRef(false)\n\n  const centerRef = useRef(center)\n  const radiusRef = useRef(radius)\n  const segmentsRef = useRef(segments)\n  const fillColorRef = useRef(fillColor)\n  const fillOpacityRef = useRef(fillOpacity)\n  const strokeColorRef = useRef(strokeColor)\n  const strokeWidthRef = useRef(strokeWidth)\n  const strokeOpacityRef = useRef(strokeOpacity)\n  const dashArrayRef = useRef(dashArray)\n\n  centerRef.current = center\n  radiusRef.current = radius\n  segmentsRef.current = segments\n  fillColorRef.current = fillColor\n  fillOpacityRef.current = fillOpacity\n  strokeColorRef.current = strokeColor\n  strokeWidthRef.current = strokeWidth\n  strokeOpacityRef.current = strokeOpacity\n  dashArrayRef.current = dashArray\n\n  const onDragStartRef = useRef(onDragStart)\n  const onDragRef = useRef(onDrag)\n  const onDragEndRef = useRef(onDragEnd)\n  onDragStartRef.current = onDragStart\n  onDragRef.current = onDrag\n  onDragEndRef.current = onDragEnd\n\n  useEffect(() => {\n    if (!map) {\n      return\n    }\n\n    const addLayers = (mapInstance: mapboxgl.Map) => {\n      if (mapInstance.getSource(sourceId)) {\n        return\n      }\n\n      const coordinates = generateCircleCoordinates(centerRef.current, radiusRef.current, segmentsRef.current)\n\n      mapInstance.addSource(sourceId, {\n        type: \"geojson\",\n        data: {\n          type: \"Feature\",\n          properties: {},\n          geometry: {\n            type: \"Polygon\",\n            coordinates: [coordinates],\n          },\n        },\n      })\n\n      const layers = mapInstance.getStyle().layers\n      let firstSymbolId: string | undefined\n      if (layers) {\n        for (const layer of layers) {\n          if (layer.type === \"symbol\") {\n            firstSymbolId = layer.id\n            break\n          }\n        }\n      }\n\n      mapInstance.addLayer(\n        {\n          id: fillLayerId,\n          type: \"fill\",\n          source: sourceId,\n          paint: {\n            \"fill-color\": fillColorRef.current,\n            \"fill-opacity\": fillOpacityRef.current,\n          },\n        },\n        firstSymbolId\n      )\n\n      mapInstance.addLayer(\n        {\n          id: strokeLayerId,\n          type: \"line\",\n          source: sourceId,\n          layout: {\n            \"line-join\": \"round\",\n            \"line-cap\": \"round\",\n          },\n          paint: {\n            \"line-color\": strokeColorRef.current,\n            \"line-width\": strokeWidthRef.current,\n            \"line-opacity\": strokeOpacityRef.current,\n            ...(dashArrayRef.current && { \"line-dasharray\": dashArrayRef.current }),\n          },\n        },\n        firstSymbolId\n      )\n\n      initializedRef.current = true\n    }\n\n    const cleanupLayers = (mapInstance: mapboxgl.Map) => {\n      try {\n        if (mapInstance.getLayer(strokeLayerId)) {\n          mapInstance.removeLayer(strokeLayerId)\n        }\n        if (mapInstance.getLayer(fillLayerId)) {\n          mapInstance.removeLayer(fillLayerId)\n        }\n        if (mapInstance.getSource(sourceId)) {\n          mapInstance.removeSource(sourceId)\n        }\n      } catch {\n        // Layers may already be removed\n      }\n      initializedRef.current = false\n    }\n\n    const handleStyleLoad = () => {\n      initializedRef.current = false\n      addLayers(map)\n    }\n\n    if (isLoaded && !initializedRef.current) {\n      addLayers(map)\n    }\n\n    map.on(\"style.load\", handleStyleLoad)\n\n    return () => {\n      map.off(\"style.load\", handleStyleLoad)\n      cleanupLayers(map)\n    }\n  }, [map, isLoaded, sourceId, fillLayerId, strokeLayerId])\n\n  useEffect(() => {\n    if (!map || !initializedRef.current) {\n      return\n    }\n\n    try {\n      const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource\n      if (source) {\n        const coordinates = generateCircleCoordinates(center, radius, segments)\n        source.setData({\n          type: \"Feature\",\n          properties: {},\n          geometry: {\n            type: \"Polygon\",\n            coordinates: [coordinates],\n          },\n        })\n      }\n    } catch {\n      // Source may not exist during style transition\n    }\n  }, [map, sourceId, center, radius, segments])\n\n  useEffect(() => {\n    if (!map || !initializedRef.current) {\n      return\n    }\n\n    try {\n      if (map.getLayer(fillLayerId)) {\n        map.setPaintProperty(fillLayerId, \"fill-color\", fillColor)\n        map.setPaintProperty(fillLayerId, \"fill-opacity\", fillOpacity)\n      }\n    } catch {\n      // Layer may not exist during style transition\n    }\n  }, [map, fillLayerId, fillColor, fillOpacity])\n\n  useEffect(() => {\n    if (!map || !initializedRef.current) {\n      return\n    }\n\n    try {\n      if (map.getLayer(strokeLayerId)) {\n        map.setPaintProperty(strokeLayerId, \"line-color\", strokeColor)\n        map.setPaintProperty(strokeLayerId, \"line-width\", strokeWidth)\n        map.setPaintProperty(strokeLayerId, \"line-opacity\", strokeOpacity)\n        if (dashArray) {\n          map.setPaintProperty(strokeLayerId, \"line-dasharray\", dashArray)\n        }\n      }\n    } catch {\n      // Layer may not exist during style transition\n    }\n  }, [map, strokeLayerId, strokeColor, strokeWidth, strokeOpacity, dashArray])\n\n  useEffect(() => {\n    if (!map || !isLoaded || !draggable) {\n      return\n    }\n\n    const defaultCursor = cursor || \"grab\"\n    const draggingCursor = \"grabbing\"\n\n    const handleMouseEnter = () => {\n      if (!isDraggingRef.current) {\n        map.getCanvas().style.cursor = defaultCursor\n      }\n    }\n\n    const handleMouseLeave = () => {\n      if (!isDraggingRef.current) {\n        map.getCanvas().style.cursor = \"\"\n      }\n    }\n\n    const handleMouseDown = (e: mapboxgl.MapLayerMouseEvent) => {\n      e.preventDefault()\n      isDraggingRef.current = true\n      map.getCanvas().style.cursor = draggingCursor\n      map.dragPan.disable()\n\n      const currentCenter = centerRef.current\n      onDragStartRef.current?.(currentCenter)\n    }\n\n    const handleMouseMove = (e: mapboxgl.MapMouseEvent) => {\n      if (!isDraggingRef.current) {\n        return\n      }\n\n      const newCenter: MapCoordinates = [e.lngLat.lng, e.lngLat.lat]\n      onDragRef.current?.(newCenter)\n    }\n\n    const handleMouseUp = (e: mapboxgl.MapMouseEvent) => {\n      if (!isDraggingRef.current) {\n        return\n      }\n\n      isDraggingRef.current = false\n      map.getCanvas().style.cursor = defaultCursor\n      map.dragPan.enable()\n\n      const newCenter: MapCoordinates = [e.lngLat.lng, e.lngLat.lat]\n      onDragEndRef.current?.(newCenter)\n    }\n\n    map.on(\"mouseenter\", fillLayerId, handleMouseEnter)\n    map.on(\"mouseleave\", fillLayerId, handleMouseLeave)\n    map.on(\"mousedown\", fillLayerId, handleMouseDown)\n    map.on(\"mousemove\", handleMouseMove)\n    map.on(\"mouseup\", handleMouseUp)\n\n    return () => {\n      map.off(\"mouseenter\", fillLayerId, handleMouseEnter)\n      map.off(\"mouseleave\", fillLayerId, handleMouseLeave)\n      map.off(\"mousedown\", fillLayerId, handleMouseDown)\n      map.off(\"mousemove\", handleMouseMove)\n      map.off(\"mouseup\", handleMouseUp)\n      try {\n        map.getCanvas().style.cursor = \"\"\n        if (isDraggingRef.current) {\n          map.dragPan.enable()\n        }\n      } catch {\n        // Map may be destroyed during cleanup\n      }\n      isDraggingRef.current = false\n    }\n  }, [map, isLoaded, draggable, fillLayerId, cursor])\n\n  return null\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/circle.tsx"
    }
  ],
  "type": "registry:ui"
}
