{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "line-animated",
  "title": "Map Line Animated",
  "description": "Animated path and route animations on the map.",
  "dependencies": ["mapbox-gl"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/line-animated.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\"\nimport { createPortal } from \"react-dom\"\nimport type mapboxgl from \"mapbox-gl\"\nimport { mapgl } from \"./map-library\"\nimport { useMap } from \"./hooks\"\nimport type { MapPath } from \"./types\"\n\ntype MapLineAnimatedProps = {\n  id: string\n  path: MapPath\n  color?: string\n  width?: number\n  opacity?: number\n  dashArray?: [number, number]\n  duration?: number\n  showMarker?: boolean\n  markerColor?: string\n  markerIcon?: ReactNode\n  markerBorderless?: boolean\n  autoStart?: boolean\n  loop?: boolean\n  onComplete?: () => void\n}\n\nconst DEFAULT_COLOR = \"#3b82f6\"\nconst DEFAULT_WIDTH = 4\nconst DEFAULT_OPACITY = 1\nconst DEFAULT_DURATION = 3000\nconst DEFAULT_SHOW_MARKER = true\nconst DEFAULT_MARKER_COLOR = \"#3b82f6\"\nconst DEFAULT_MARKER_BORDERLESS = false\nconst DEFAULT_AUTO_START = true\nconst DEFAULT_LOOP = false\nconst LOOP_RESTART_DELAY_MS = 500\n\nexport const MapLineAnimated = ({\n  id,\n  path,\n  color = DEFAULT_COLOR,\n  width = DEFAULT_WIDTH,\n  opacity = DEFAULT_OPACITY,\n  dashArray,\n  duration = DEFAULT_DURATION,\n  showMarker = DEFAULT_SHOW_MARKER,\n  markerColor = DEFAULT_MARKER_COLOR,\n  markerIcon,\n  markerBorderless = DEFAULT_MARKER_BORDERLESS,\n  autoStart = DEFAULT_AUTO_START,\n  loop = DEFAULT_LOOP,\n  onComplete,\n}: MapLineAnimatedProps) => {\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 htmlMarkerRef = useRef<mapboxgl.Marker | null>(null)\n  const markerElementRef = useRef<HTMLDivElement | null>(null)\n  const startTimeRef = useRef<number>(0)\n  const durationRef = useRef(duration)\n  const pathRef = useRef(path)\n  const colorRef = useRef(color)\n  const widthRef = useRef(width)\n  const opacityRef = useRef(opacity)\n  const dashArrayRef = useRef(dashArray)\n  const showMarkerRef = useRef(showMarker)\n  const markerColorRef = useRef(markerColor)\n  const markerBorderlessRef = useRef(markerBorderless)\n  const markerIconRef = useRef(markerIcon)\n  const onCompleteRef = useRef(onComplete)\n  const autoStartRef = useRef(autoStart)\n\n  durationRef.current = duration\n  pathRef.current = path\n  colorRef.current = color\n  widthRef.current = width\n  opacityRef.current = opacity\n  dashArrayRef.current = dashArray\n  showMarkerRef.current = showMarker\n  markerColorRef.current = markerColor\n  markerBorderlessRef.current = markerBorderless\n  markerIconRef.current = markerIcon\n  onCompleteRef.current = onComplete\n  autoStartRef.current = autoStart\n\n  const [isAnimating, setIsAnimating] = useState(false)\n  const [isMarkerMounted, setIsMarkerMounted] = useState(false)\n  const hasCompletedRef = useRef(false)\n\n  const sourceId = `${id}-source`\n  const lineLayerId = `${id}-line`\n  const markerSourceId = `${id}-marker-source`\n  const markerLayerId = `${id}-marker`\n\n  useEffect(() => {\n    if (!map) {\n      return\n    }\n\n    const addLineSource = (mapInstance: mapboxgl.Map) => {\n      if (mapInstance.getSource(sourceId)) {\n        return\n      }\n\n      mapInstance.addSource(sourceId, {\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: sourceId,\n        layout: { \"line-join\": \"round\", \"line-cap\": \"round\" },\n        paint: {\n          \"line-color\": colorRef.current,\n          \"line-width\": widthRef.current,\n          \"line-opacity\": opacityRef.current,\n          ...(dashArrayRef.current && { \"line-dasharray\": dashArrayRef.current }),\n        },\n      })\n    }\n\n    const addHtmlMarker = (mapInstance: mapboxgl.Map) => {\n      if (htmlMarkerRef.current) {\n        return\n      }\n\n      const el = document.createElement(\"div\")\n      el.style.width = \"32px\"\n      el.style.height = \"32px\"\n      el.style.borderRadius = \"50%\"\n      el.style.backgroundColor = markerColorRef.current\n      el.style.display = \"flex\"\n      el.style.alignItems = \"center\"\n      el.style.justifyContent = \"center\"\n\n      if (!markerBorderlessRef.current) {\n        el.style.boxShadow = \"0 0 0 3px white\"\n      }\n\n      markerElementRef.current = el\n      htmlMarkerRef.current = new mapgl.Marker(el).setLngLat(pathRef.current[0]).addTo(mapInstance)\n      setIsMarkerMounted(true)\n    }\n\n    const addCircleMarker = (mapInstance: mapboxgl.Map) => {\n      if (mapInstance.getSource(markerSourceId)) {\n        return\n      }\n\n      mapInstance.addSource(markerSourceId, {\n        type: \"geojson\",\n        data: {\n          type: \"Feature\",\n          properties: {},\n          geometry: { type: \"Point\", coordinates: pathRef.current[0] },\n        },\n      })\n\n      mapInstance.addLayer({\n        id: markerLayerId,\n        type: \"circle\",\n        source: markerSourceId,\n        paint: {\n          \"circle-radius\": 8,\n          \"circle-color\": markerColorRef.current,\n          \"circle-stroke-width\": markerBorderlessRef.current ? 0 : 3,\n          \"circle-stroke-color\": \"#ffffff\",\n        },\n      })\n    }\n\n    const addSources = (mapInstance: mapboxgl.Map) => {\n      try {\n        addLineSource(mapInstance)\n\n        if (showMarkerRef.current) {\n          if (markerIconRef.current) {\n            addHtmlMarker(mapInstance)\n          } else {\n            addCircleMarker(mapInstance)\n          }\n        }\n\n        initializedRef.current = true\n      } catch (error) {\n        console.error(\"Error adding animated line:\", error)\n      }\n    }\n\n    const cleanupResources = (mapInstance: mapboxgl.Map) => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n      }\n\n      if (htmlMarkerRef.current) {\n        htmlMarkerRef.current.remove()\n        htmlMarkerRef.current = null\n      }\n\n      try {\n        if (mapInstance.getLayer(lineLayerId)) {\n          mapInstance.removeLayer(lineLayerId)\n        }\n        if (mapInstance.getLayer(markerLayerId)) {\n          mapInstance.removeLayer(markerLayerId)\n        }\n        if (mapInstance.getSource(sourceId)) {\n          mapInstance.removeSource(sourceId)\n        }\n        if (mapInstance.getSource(markerSourceId)) {\n          mapInstance.removeSource(markerSourceId)\n        }\n      } catch {\n        // Layer or source may already be removed\n      }\n\n      markerElementRef.current = null\n      setIsMarkerMounted(false)\n      initializedRef.current = false\n    }\n\n    const handleStyleLoad = () => {\n      styleLoadedRef.current = true\n      initializedRef.current = false\n      addSources(map)\n\n      if (autoStartRef.current) {\n        setIsAnimating(true)\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, sourceId, lineLayerId, markerSourceId, markerLayerId])\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\", color)\n      }\n    } catch {\n      // Layer may not exist during style transition\n    }\n  }, [map, lineLayerId, color])\n\n  useEffect(() => {\n    if (!map || !initializedRef.current) {\n      return\n    }\n\n    try {\n      if (map.getLayer(lineLayerId)) {\n        map.setPaintProperty(lineLayerId, \"line-width\", width)\n      }\n    } catch {\n      // Layer may not exist during style transition\n    }\n  }, [map, lineLayerId, width])\n\n  useEffect(() => {\n    if (!map || !initializedRef.current) {\n      return\n    }\n\n    try {\n      if (map.getLayer(lineLayerId)) {\n        map.setPaintProperty(lineLayerId, \"line-opacity\", opacity)\n      }\n    } catch {\n      // Layer may not exist during style transition\n    }\n  }, [map, lineLayerId, opacity])\n\n  useEffect(() => {\n    if (!map || !initializedRef.current || !dashArray) {\n      return\n    }\n\n    try {\n      if (map.getLayer(lineLayerId)) {\n        map.setPaintProperty(lineLayerId, \"line-dasharray\", dashArray)\n      }\n    } catch {\n      // Layer may not exist during style transition\n    }\n  }, [map, lineLayerId, dashArray])\n\n  useEffect(() => {\n    if (!map || !initializedRef.current) {\n      return\n    }\n\n    try {\n      if (markerIconRef.current && htmlMarkerRef.current) {\n        htmlMarkerRef.current.getElement().style.backgroundColor = markerColor\n      } else if (map.getLayer(markerLayerId)) {\n        map.setPaintProperty(markerLayerId, \"circle-color\", markerColor)\n      }\n    } catch {\n      // Layer may not exist during style transition\n    }\n  }, [map, markerLayerId, markerColor])\n\n  useEffect(() => {\n    if (!map || !initializedRef.current) {\n      return\n    }\n\n    try {\n      if (markerIconRef.current && htmlMarkerRef.current) {\n        htmlMarkerRef.current.getElement().style.boxShadow = markerBorderless ? \"none\" : \"0 0 0 3px white\"\n      } else if (map.getLayer(markerLayerId)) {\n        map.setPaintProperty(markerLayerId, \"circle-stroke-width\", markerBorderless ? 0 : 3)\n      }\n    } catch {\n      // Layer may not exist during style transition\n    }\n  }, [map, markerLayerId, markerBorderless])\n\n  useEffect(() => {\n    if (!map || !isLoaded || !initializedRef.current) {\n      return\n    }\n    if (!autoStart && !isAnimating) {\n      return\n    }\n    if (!isAnimating && hasCompletedRef.current) {\n      return\n    }\n\n    startTimeRef.current = Date.now()\n\n    const updateLineSource = (coordinates: MapPath) => {\n      const lineSource = map.getSource(sourceId) as mapboxgl.GeoJSONSource\n      if (lineSource) {\n        lineSource.setData({\n          type: \"Feature\",\n          properties: {},\n          geometry: { type: \"LineString\", coordinates },\n        })\n      }\n    }\n\n    const updateMarkerPosition = (position: [number, number]) => {\n      if (markerIconRef.current && htmlMarkerRef.current) {\n        htmlMarkerRef.current.setLngLat(position)\n        return\n      }\n\n      const markerSource = map.getSource(markerSourceId) as mapboxgl.GeoJSONSource\n      if (markerSource) {\n        markerSource.setData({\n          type: \"Feature\",\n          properties: {},\n          geometry: { type: \"Point\", coordinates: position },\n        })\n      }\n    }\n\n    const stopAnimation = () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n        animationFrameRef.current = undefined\n      }\n      setIsAnimating(false)\n    }\n\n    const animate = () => {\n      if (!map || !styleLoadedRef.current) {\n        stopAnimation()\n        return\n      }\n\n      const elapsed = Date.now() - startTimeRef.current\n      const progress = Math.min(elapsed / durationRef.current, 1)\n      const totalPoints = pathRef.current.length\n      const currentPointIndex = Math.floor(progress * (totalPoints - 1))\n      const segmentProgress = (progress * (totalPoints - 1)) % 1\n      const visibleCoordinates = pathRef.current.slice(0, currentPointIndex + 1)\n\n      if (currentPointIndex < totalPoints - 1 && segmentProgress > 0) {\n        const start = pathRef.current[currentPointIndex]\n        const end = pathRef.current[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 (showMarkerRef.current && visibleCoordinates.length > 0) {\n        try {\n          const lastPoint = visibleCoordinates[visibleCoordinates.length - 1]\n          updateMarkerPosition(lastPoint as [number, number])\n        } catch {\n          stopAnimation()\n          return\n        }\n      }\n\n      if (progress < 1) {\n        animationFrameRef.current = requestAnimationFrame(animate)\n        return\n      }\n\n      setIsAnimating(false)\n      hasCompletedRef.current = true\n      onCompleteRef.current?.()\n\n      if (loop) {\n        setTimeout(() => {\n          hasCompletedRef.current = false\n          setIsAnimating(true)\n        }, LOOP_RESTART_DELAY_MS)\n      }\n    }\n\n    setIsAnimating(true)\n    animationFrameRef.current = requestAnimationFrame(animate)\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n      }\n    }\n  }, [map, isLoaded, sourceId, markerSourceId, autoStart, loop, isAnimating])\n\n  if (markerIcon && isMarkerMounted && markerElementRef.current) {\n    return createPortal(markerIcon, markerElementRef.current)\n  }\n\n  return null\n}\n\nexport const useLineAnimatedControl = () => {\n  const [isPlaying, setIsPlaying] = useState(false)\n\n  const start = () => {\n    setIsPlaying(true)\n  }\n\n  const stop = () => {\n    setIsPlaying(false)\n  }\n\n  const toggle = () => {\n    setIsPlaying((prev) => {\n      return !prev\n    })\n  }\n\n  return { start, stop, toggle, isPlaying }\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/line-animated.tsx"
    }
  ],
  "type": "registry:ui"
}
