{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "marker-animated",
  "title": "Map Marker Animated",
  "description": "Animated marker that moves along a path with trail visualization.",
  "dependencies": ["mapbox-gl"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/marker-animated.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { useMap } from \"./hooks\"\nimport type { MapPath } from \"./types\"\n\ntype MapMarkerAnimatedProps = {\n  /** Unique identifier for the marker */\n  id: string\n  /** Array of coordinates [[lng, lat], ...] defining the path */\n  coordinates: MapPath\n  /** Marker color */\n  color?: string\n  /** Marker size (radius in pixels) */\n  size?: number\n  /** Animation duration in milliseconds */\n  duration?: number\n  /** Auto-start animation on mount */\n  autoStart?: boolean\n  /** Loop animation */\n  loop?: boolean\n  /** Show the path/route line */\n  showPath?: boolean\n  /** Path line color */\n  pathColor?: string\n  /** Path line width */\n  pathWidth?: number\n  /** Callback when animation completes */\n  onComplete?: () => void\n}\n\nexport function MapMarkerAnimated({\n  id,\n  coordinates,\n  color = \"#3b82f6\",\n  size = 10,\n  duration = 5000,\n  autoStart = true,\n  loop = false,\n  showPath = false,\n  pathColor = \"#3b82f6\",\n  pathWidth = 4,\n  onComplete,\n}: MapMarkerAnimatedProps) {\n  const { map, isLoaded } = useMap()\n  const initializedRef = useRef(false)\n  const [isAnimating, setIsAnimating] = useState(false)\n  const animationFrameRef = useRef<number | undefined>(undefined)\n  const loopTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const onCompleteRef = useRef(onComplete)\n  onCompleteRef.current = onComplete\n\n  // Initialize marker and path layers\n  useEffect(() => {\n    if (!map || !isLoaded || initializedRef.current) return\n\n    const markerSourceId = `${id}-marker-source`\n    const markerLayerId = `${id}-marker`\n    const pathSourceId = `${id}-path-source`\n    const pathLayerId = `${id}-path`\n\n    try {\n      // Add marker source\n      map.addSource(markerSourceId, {\n        type: \"geojson\",\n        data: {\n          type: \"Feature\",\n          properties: {},\n          geometry: {\n            type: \"Point\",\n            coordinates: coordinates[0],\n          },\n        },\n      })\n\n      // Add marker layer\n      map.addLayer({\n        id: markerLayerId,\n        type: \"circle\",\n        source: markerSourceId,\n        paint: {\n          \"circle-radius\": size,\n          \"circle-color\": color,\n          \"circle-stroke-width\": 2,\n          \"circle-stroke-color\": \"#ffffff\",\n        },\n      })\n\n      // Add path if enabled\n      if (showPath) {\n        map.addSource(pathSourceId, {\n          type: \"geojson\",\n          data: {\n            type: \"Feature\",\n            properties: {},\n            geometry: {\n              type: \"LineString\",\n              coordinates: coordinates,\n            },\n          },\n        })\n\n        map.addLayer({\n          id: pathLayerId,\n          type: \"line\",\n          source: pathSourceId,\n          layout: {\n            \"line-join\": \"round\",\n            \"line-cap\": \"round\",\n          },\n          paint: {\n            \"line-color\": pathColor,\n            \"line-width\": pathWidth,\n            \"line-opacity\": 0.6,\n          },\n        })\n      }\n\n      initializedRef.current = true\n    } catch (error) {\n      console.error(\"Error adding animated marker:\", error)\n    }\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n      }\n      if (map) {\n        try {\n          if (map.getLayer && map.getLayer(markerLayerId)) map.removeLayer(markerLayerId)\n          if (showPath && map.getLayer && map.getLayer(pathLayerId)) map.removeLayer(pathLayerId)\n          if (map.getSource && map.getSource(markerSourceId)) map.removeSource(markerSourceId)\n          if (showPath && map.getSource && map.getSource(pathSourceId)) map.removeSource(pathSourceId)\n        } catch {\n          // Silently catch errors during cleanup\n        }\n      }\n      initializedRef.current = false\n    }\n  }, [map, isLoaded, id, coordinates, color, size, showPath, pathColor, pathWidth])\n\n  // Animation logic\n  useEffect(() => {\n    if (!map || !isLoaded || !initializedRef.current) return\n    if (!autoStart && !isAnimating) return\n\n    const markerSourceId = `${id}-marker-source`\n    const startTime = Date.now()\n\n    const animate = () => {\n      // Guard: Check if map is still valid\n      if (!map || !map.getStyle()) {\n        if (animationFrameRef.current) {\n          cancelAnimationFrame(animationFrameRef.current)\n          animationFrameRef.current = undefined\n        }\n        setIsAnimating(false)\n        return\n      }\n\n      const elapsed = Date.now() - startTime\n      const progress = Math.min(elapsed / duration, 1)\n\n      // Calculate position along the route\n      const totalSegments = coordinates.length - 1\n      const segmentIndex = progress * totalSegments\n      const currentSegment = Math.floor(segmentIndex)\n      const segmentProgress = segmentIndex % 1\n\n      // Use the last segment when at 100% progress\n      const segIndex = currentSegment >= totalSegments ? totalSegments - 1 : currentSegment\n      const segProgress = currentSegment >= totalSegments ? 1 : segmentProgress\n\n      // Calculate interpolated position\n      const start = coordinates[segIndex]\n      const end = coordinates[segIndex + 1]\n      const lng = start[0] + (end[0] - start[0]) * segProgress\n      const lat = start[1] + (end[1] - start[1]) * segProgress\n\n      // Update marker position\n      try {\n        const markerSource = map.getSource(markerSourceId) as mapboxgl.GeoJSONSource\n        if (markerSource) {\n          markerSource.setData({\n            type: \"Feature\",\n            properties: {},\n            geometry: {\n              type: \"Point\",\n              coordinates: [lng, lat],\n            },\n          })\n        }\n      } catch {\n        console.warn(\"Marker source no longer available, stopping animation\")\n        if (animationFrameRef.current) {\n          cancelAnimationFrame(animationFrameRef.current)\n          animationFrameRef.current = undefined\n        }\n        setIsAnimating(false)\n        return\n      }\n\n      if (progress < 1) {\n        animationFrameRef.current = requestAnimationFrame(animate)\n      } else {\n        setIsAnimating(false)\n        onCompleteRef.current?.()\n\n        if (loop) {\n          loopTimerRef.current = setTimeout(() => {\n            setIsAnimating(true)\n          }, 500)\n        }\n      }\n    }\n\n    setIsAnimating(true)\n    animationFrameRef.current = requestAnimationFrame(animate)\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n      }\n      if (loopTimerRef.current !== null) {\n        clearTimeout(loopTimerRef.current)\n      }\n    }\n  }, [map, isLoaded, id, coordinates, duration, autoStart, isAnimating, loop])\n\n  return null\n}\n\n/**\n * Hook to control animated marker playback\n */\nexport function useMarkerAnimatedControl() {\n  const [isPlaying, setIsPlaying] = useState(false)\n\n  const start = () => setIsPlaying(true)\n  const stop = () => setIsPlaying(false)\n  const toggle = () => setIsPlaying((prev) => !prev)\n\n  return { start, stop, toggle, isPlaying }\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/marker-animated.tsx"
    }
  ],
  "type": "registry:ui"
}
