{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-circle",
  "title": "Map Animated Circle",
  "description": "Animated circle with drawing and fill effects.",
  "dependencies": ["mapbox-gl"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/animated-circle.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport type mapboxgl from \"mapbox-gl\"\n\nimport { useMap } from \"./hooks\"\nimport type { MapCoordinates, MapPath } from \"./types\"\n\ntype AnimationMode = \"draw\" | \"fill\"\n\ntype MapAnimatedCircleProps = {\n  id: string\n  center: MapCoordinates\n  radius: number\n  strokeColor?: string\n  strokeWidth?: number\n  strokeOpacity?: number\n  strokeDashArray?: number[]\n  fillColor?: string\n  fillOpacity?: number\n  duration?: number\n  fillDuration?: number\n  autoStart?: boolean\n  loop?: boolean\n  loopDelay?: number\n  animationMode?: AnimationMode\n  segments?: number\n  onDrawComplete?: () => void\n  onFillComplete?: () => void\n  onComplete?: () => void\n}\n\nconst DEFAULT_STROKE_COLOR = \"#3b82f6\"\nconst DEFAULT_STROKE_WIDTH = 2\nconst DEFAULT_STROKE_OPACITY = 1\nconst DEFAULT_FILL_COLOR = \"#3b82f6\"\nconst DEFAULT_FILL_OPACITY = 0.3\nconst DEFAULT_DURATION = 2000\nconst DEFAULT_FILL_DURATION = 1000\nconst DEFAULT_AUTO_START = true\nconst DEFAULT_LOOP = false\nconst DEFAULT_LOOP_DELAY = 1000\nconst DEFAULT_ANIMATION_MODE: AnimationMode = \"draw\"\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 MapAnimatedCircle = ({\n  id,\n  center,\n  radius,\n  strokeColor = DEFAULT_STROKE_COLOR,\n  strokeWidth = DEFAULT_STROKE_WIDTH,\n  strokeOpacity = DEFAULT_STROKE_OPACITY,\n  strokeDashArray,\n  fillColor = DEFAULT_FILL_COLOR,\n  fillOpacity = DEFAULT_FILL_OPACITY,\n  duration = DEFAULT_DURATION,\n  fillDuration = DEFAULT_FILL_DURATION,\n  autoStart = DEFAULT_AUTO_START,\n  loop = DEFAULT_LOOP,\n  loopDelay = DEFAULT_LOOP_DELAY,\n  animationMode = DEFAULT_ANIMATION_MODE,\n  segments = DEFAULT_SEGMENTS,\n  onDrawComplete,\n  onFillComplete,\n  onComplete,\n}: MapAnimatedCircleProps) => {\n  const { map, isLoaded } = useMap()\n\n  const initializedRef = useRef(false)\n  const styleLoadedRef = useRef(false)\n  const animationFrameRef = useRef<number | undefined>(undefined)\n  const loopTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n  const startTimeRef = useRef<number>(0)\n  const hasCompletedRef = useRef(false)\n\n  const centerRef = useRef(center)\n  const radiusRef = useRef(radius)\n  const segmentsRef = useRef(segments)\n  const durationRef = useRef(duration)\n  const fillDurationRef = useRef(fillDuration)\n  const onDrawCompleteRef = useRef(onDrawComplete)\n  const onFillCompleteRef = useRef(onFillComplete)\n  const onCompleteRef = useRef(onComplete)\n\n  centerRef.current = center\n  radiusRef.current = radius\n  segmentsRef.current = segments\n  durationRef.current = duration\n  fillDurationRef.current = fillDuration\n  onDrawCompleteRef.current = onDrawComplete\n  onFillCompleteRef.current = onFillComplete\n  onCompleteRef.current = onComplete\n\n  const [isAnimating, setIsAnimating] = useState(false)\n  const [animationPhase, setAnimationPhase] = useState<\"idle\" | \"drawing\" | \"filling\">(\"idle\")\n\n  const lineSourceId = `${id}-line-source`\n  const lineLayerId = `${id}-line`\n  const fillSourceId = `${id}-fill-source`\n  const fillLayerId = `${id}-fill`\n\n  useEffect(() => {\n    if (!map) {\n      return\n    }\n\n    const addLineSources = (mapInstance: mapboxgl.Map) => {\n      if (mapInstance.getSource(lineSourceId)) {\n        return\n      }\n\n      mapInstance.addSource(lineSourceId, {\n        type: \"geojson\",\n        data: {\n          type: \"Feature\",\n          properties: {},\n          geometry: { type: \"LineString\", coordinates: [] },\n        },\n      })\n\n      mapInstance.addLayer({\n        id: lineLayerId,\n        type: \"line\",\n        source: lineSourceId,\n        layout: { \"line-join\": \"round\", \"line-cap\": \"round\" },\n        paint: {\n          \"line-color\": strokeColor,\n          \"line-width\": strokeWidth,\n          \"line-opacity\": strokeOpacity,\n          ...(strokeDashArray && { \"line-dasharray\": strokeDashArray }),\n        },\n      })\n    }\n\n    const addFillSources = (mapInstance: mapboxgl.Map) => {\n      if (mapInstance.getSource(fillSourceId)) {\n        return\n      }\n\n      mapInstance.addSource(fillSourceId, {\n        type: \"geojson\",\n        data: {\n          type: \"Feature\",\n          properties: {},\n          geometry: { type: \"Polygon\", coordinates: [[]] },\n        },\n      })\n\n      mapInstance.addLayer(\n        {\n          id: fillLayerId,\n          type: \"fill\",\n          source: fillSourceId,\n          paint: {\n            \"fill-color\": fillColor,\n            \"fill-opacity\": 0,\n          },\n        },\n        lineLayerId\n      )\n    }\n\n    const addSources = (mapInstance: mapboxgl.Map) => {\n      try {\n        addLineSources(mapInstance)\n        if (animationMode !== \"draw\") {\n          addFillSources(mapInstance)\n        }\n        initializedRef.current = true\n      } catch (error) {\n        console.error(\"Error adding animated circle:\", error)\n      }\n    }\n\n    const cleanupResources = (mapInstance: mapboxgl.Map) => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n      }\n      if (loopTimeoutRef.current) {\n        clearTimeout(loopTimeoutRef.current)\n      }\n\n      try {\n        if (mapInstance.getLayer(lineLayerId)) {\n          mapInstance.removeLayer(lineLayerId)\n        }\n        if (mapInstance.getLayer(fillLayerId)) {\n          mapInstance.removeLayer(fillLayerId)\n        }\n        if (mapInstance.getSource(lineSourceId)) {\n          mapInstance.removeSource(lineSourceId)\n        }\n        if (mapInstance.getSource(fillSourceId)) {\n          mapInstance.removeSource(fillSourceId)\n        }\n      } catch {\n        // Resources may already be removed\n      }\n\n      initializedRef.current = false\n    }\n\n    const handleStyleLoad = () => {\n      styleLoadedRef.current = true\n      initializedRef.current = false\n      addSources(map)\n\n      if (autoStart) {\n        setIsAnimating(true)\n        setAnimationPhase(\"drawing\")\n      }\n    }\n\n    const handleStyleDataLoading = () => {\n      styleLoadedRef.current = false\n    }\n\n    if (isLoaded && !initializedRef.current) {\n      addSources(map)\n      styleLoadedRef.current = true\n    }\n\n    map.on(\"style.load\", handleStyleLoad)\n    map.on(\"styledataloading\", handleStyleDataLoading)\n\n    return () => {\n      map.off(\"style.load\", handleStyleLoad)\n      map.off(\"styledataloading\", handleStyleDataLoading)\n      cleanupResources(map)\n    }\n  }, [map, isLoaded, id, animationMode, strokeColor, strokeWidth, strokeOpacity, strokeDashArray, fillColor])\n\n  useEffect(() => {\n    if (!map || !initializedRef.current) {\n      return\n    }\n\n    try {\n      if (map.getLayer(lineLayerId)) {\n        map.setPaintProperty(lineLayerId, \"line-color\", strokeColor)\n        map.setPaintProperty(lineLayerId, \"line-width\", strokeWidth)\n        map.setPaintProperty(lineLayerId, \"line-opacity\", strokeOpacity)\n        if (strokeDashArray) {\n          map.setPaintProperty(lineLayerId, \"line-dasharray\", strokeDashArray)\n        }\n      }\n    } catch {\n      // Layer may not exist during style transition\n    }\n  }, [map, id, strokeColor, strokeWidth, strokeOpacity, strokeDashArray])\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      }\n    } catch {\n      // Layer may not exist during style transition\n    }\n  }, [map, id, fillColor])\n\n  useEffect(() => {\n    if (!map || !isLoaded || !initializedRef.current) {\n      return\n    }\n    if (!autoStart && !isAnimating) {\n      return\n    }\n    if (hasCompletedRef.current && !loop) {\n      return\n    }\n\n    startTimeRef.current = Date.now()\n\n    const circleCoordinates = generateCircleCoordinates(centerRef.current, radiusRef.current, segmentsRef.current)\n\n    const updateLineSource = (visibleCoordinates: MapCoordinates[]) => {\n      if (!map || !map.getStyle()) {\n        return\n      }\n      try {\n        const lineSource = map.getSource(lineSourceId) as mapboxgl.GeoJSONSource\n        if (lineSource) {\n          lineSource.setData({\n            type: \"Feature\",\n            properties: {},\n            geometry: { type: \"LineString\", coordinates: visibleCoordinates },\n          })\n        }\n      } catch {\n        // Map may be in an invalid state\n      }\n    }\n\n    const updateFillSource = (fillCoordinates: MapCoordinates[]) => {\n      if (!map || !map.getStyle()) {\n        return\n      }\n      try {\n        const source = map.getSource(fillSourceId) as mapboxgl.GeoJSONSource\n        if (source) {\n          source.setData({\n            type: \"Feature\",\n            properties: {},\n            geometry: { type: \"Polygon\", coordinates: [fillCoordinates] },\n          })\n        }\n      } catch {\n        // Map may be in an invalid state\n      }\n    }\n\n    const updateFillOpacity = (opacity: number) => {\n      if (!map || !map.getStyle()) {\n        return\n      }\n      try {\n        if (map.getLayer(fillLayerId)) {\n          map.setPaintProperty(fillLayerId, \"fill-opacity\", opacity)\n        }\n      } catch {\n        // Layer may not exist during style transition\n      }\n    }\n\n    const stopAnimation = () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n        animationFrameRef.current = undefined\n      }\n      setIsAnimating(false)\n      setAnimationPhase(\"idle\")\n    }\n\n    const resetAnimation = () => {\n      updateLineSource([])\n      updateFillSource([])\n      updateFillOpacity(0)\n    }\n\n    const animate = () => {\n      if (!map || !styleLoadedRef.current) {\n        stopAnimation()\n        return\n      }\n\n      const elapsed = Date.now() - startTimeRef.current\n\n      if (animationPhase === \"drawing\") {\n        const drawProgress = Math.min(elapsed / durationRef.current, 1)\n        const totalPoints = circleCoordinates.length\n        const currentPointIndex = Math.floor(drawProgress * (totalPoints - 1))\n        const segmentProgress = (drawProgress * (totalPoints - 1)) % 1\n        const visibleCoordinates = circleCoordinates.slice(0, currentPointIndex + 1)\n\n        if (currentPointIndex < totalPoints - 1 && segmentProgress > 0) {\n          const start = circleCoordinates[currentPointIndex]\n          const end = circleCoordinates[currentPointIndex + 1]\n          visibleCoordinates.push([\n            start[0] + (end[0] - start[0]) * segmentProgress,\n            start[1] + (end[1] - start[1]) * segmentProgress,\n          ])\n        }\n\n        try {\n          updateLineSource(visibleCoordinates)\n        } catch {\n          stopAnimation()\n          return\n        }\n\n        if (drawProgress >= 1) {\n          onDrawCompleteRef.current?.()\n\n          if (animationMode === \"draw\") {\n            hasCompletedRef.current = true\n            setIsAnimating(false)\n            setAnimationPhase(\"idle\")\n            onCompleteRef.current?.()\n\n            if (loop) {\n              loopTimeoutRef.current = setTimeout(() => {\n                hasCompletedRef.current = false\n                resetAnimation()\n                setIsAnimating(true)\n                setAnimationPhase(\"drawing\")\n              }, loopDelay)\n            }\n            return\n          }\n\n          startTimeRef.current = Date.now()\n          setAnimationPhase(\"filling\")\n          updateFillSource(circleCoordinates)\n          animationFrameRef.current = requestAnimationFrame(animate)\n          return\n        }\n\n        animationFrameRef.current = requestAnimationFrame(animate)\n        return\n      }\n\n      if (animationPhase === \"filling\") {\n        const fillProgress = Math.min(elapsed / fillDurationRef.current, 1)\n        const currentFillOpacity = fillOpacity * fillProgress\n\n        try {\n          updateFillOpacity(currentFillOpacity)\n        } catch {\n          stopAnimation()\n          return\n        }\n\n        if (fillProgress >= 1) {\n          hasCompletedRef.current = true\n          onFillCompleteRef.current?.()\n          setIsAnimating(false)\n          setAnimationPhase(\"idle\")\n          onCompleteRef.current?.()\n\n          if (loop) {\n            loopTimeoutRef.current = setTimeout(() => {\n              hasCompletedRef.current = false\n              resetAnimation()\n              setIsAnimating(true)\n              setAnimationPhase(\"drawing\")\n            }, loopDelay)\n          }\n          return\n        }\n\n        animationFrameRef.current = requestAnimationFrame(animate)\n      }\n    }\n\n    setIsAnimating(true)\n    if (animationPhase === \"idle\") {\n      setAnimationPhase(\"drawing\")\n    }\n    animationFrameRef.current = requestAnimationFrame(animate)\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n      }\n      if (loopTimeoutRef.current) {\n        clearTimeout(loopTimeoutRef.current)\n      }\n    }\n  }, [map, isLoaded, id, autoStart, loop, loopDelay, animationMode, fillOpacity, isAnimating, animationPhase])\n\n  return null\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/animated-circle.tsx"
    }
  ],
  "type": "registry:ui"
}
