{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radar",
  "title": "Map Radar",
  "description": "Animated radar sweep signal overlay on map coordinates.",
  "dependencies": ["mapbox-gl"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/radar.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useId, useRef, useState } from \"react\"\nimport { createPortal } from \"react-dom\"\nimport { mapgl } from \"./map-library\"\nimport { useMap } from \"./hooks\"\nimport type { MapCoordinates } from \"./types\"\n\ntype RgbColor = { r: number; g: number; b: number }\n\ntype MapRadarProps = {\n  coordinates: MapCoordinates\n  size?: number\n  color?: string\n  gridColor?: string\n  backgroundColor?: string\n  duration?: number\n  rings?: number\n  showCrosshairs?: boolean\n  pitchAlignment?: \"map\" | \"viewport\" | \"auto\"\n}\n\ntype RadarContentProps = {\n  instanceId: string\n  size: number\n  color: string\n  gridColor: string\n  backgroundColor: string\n  duration: number\n  rings: number\n  showCrosshairs: boolean\n}\n\ntype RadarGridProps = {\n  radius: number\n  gridColor: string\n  rings: number\n  showCrosshairs: boolean\n}\n\ntype RadarSweepProps = {\n  instanceId: string\n  radius: number\n  color: string\n  duration: number\n}\n\ntype SweepGradientProps = {\n  radius: number\n  sweepRadius: number\n  rgb: RgbColor\n}\n\ntype SweepSegmentProps = {\n  radius: number\n  sweepRadius: number\n  rgb: RgbColor\n  segmentIndex: number\n}\n\nconst DEFAULT_SIZE = 200\nconst DEFAULT_COLOR = \"rgba(0, 255, 70, 1)\"\nconst DEFAULT_GRID_COLOR = \"rgba(0, 255, 70, 0.3)\"\nconst DEFAULT_BACKGROUND_COLOR = \"rgba(0, 20, 0, 0.8)\"\nconst DEFAULT_DURATION = 2000\nconst DEFAULT_RINGS = 4\nconst DEFAULT_CROSSHAIRS = true\nconst DEFAULT_PITCH_ALIGNMENT = \"map\" as const\nconst SWEEP_SEGMENT_COUNT = 20\nconst SWEEP_ARC = Math.PI / 2.5\n\nconst createMarker = (\n  map: mapboxgl.Map,\n  container: HTMLDivElement,\n  coordinates: MapCoordinates,\n  pitchAlignment: \"map\" | \"viewport\" | \"auto\"\n) => {\n  return new mapgl.Marker({\n    element: container,\n    anchor: \"center\",\n    pitchAlignment,\n  })\n    .setLngLat(coordinates)\n    .addTo(map)\n}\n\nexport const MapRadar = ({\n  coordinates,\n  size = DEFAULT_SIZE,\n  color = DEFAULT_COLOR,\n  gridColor = DEFAULT_GRID_COLOR,\n  backgroundColor = DEFAULT_BACKGROUND_COLOR,\n  duration = DEFAULT_DURATION,\n  rings = DEFAULT_RINGS,\n  showCrosshairs = DEFAULT_CROSSHAIRS,\n  pitchAlignment = DEFAULT_PITCH_ALIGNMENT,\n}: MapRadarProps) => {\n  const { map, isLoaded } = useMap()\n  const instanceId = useId()\n  const markerRef = useRef<mapboxgl.Marker | null>(null)\n  const containerRef = useRef<HTMLDivElement | null>(null)\n  const [isMounted, setIsMounted] = useState(false)\n\n  useEffect(() => {\n    if (!map || !isLoaded) {\n      return\n    }\n\n    const container = document.createElement(\"div\")\n    containerRef.current = container\n\n    const marker = createMarker(map, container, coordinates, pitchAlignment)\n    markerRef.current = marker\n    setIsMounted(true)\n\n    return () => {\n      marker.remove()\n      markerRef.current = null\n      containerRef.current = null\n      setIsMounted(false)\n    }\n  }, [map, isLoaded, pitchAlignment])\n\n  useEffect(() => {\n    if (markerRef.current) {\n      markerRef.current.setLngLat(coordinates)\n    }\n  }, [coordinates])\n\n  if (!isMounted || !containerRef.current) {\n    return null\n  }\n\n  return createPortal(\n    <RadarContent\n      instanceId={instanceId}\n      size={size}\n      color={color}\n      gridColor={gridColor}\n      backgroundColor={backgroundColor}\n      duration={duration}\n      rings={rings}\n      showCrosshairs={showCrosshairs}\n    />,\n    containerRef.current\n  )\n}\n\nconst RadarContent = ({\n  instanceId,\n  size,\n  color,\n  gridColor,\n  backgroundColor,\n  duration,\n  rings,\n  showCrosshairs,\n}: RadarContentProps) => {\n  const radius = size / 2\n  const clipId = `radar-clip-${instanceId}`\n\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        width: size,\n        height: size,\n      }}\n    >\n      <style>{`@keyframes radar-sweep{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}`}</style>\n      <svg\n        width={size}\n        height={size}\n        viewBox={`0 0 ${size} ${size}`}\n        style={{ position: \"absolute\", inset: 0 }}\n      >\n        <defs>\n          <clipPath id={clipId}>\n            <circle cx={radius} cy={radius} r={radius - 2} />\n          </clipPath>\n        </defs>\n\n        <circle cx={radius} cy={radius} r={radius - 2} fill={backgroundColor} />\n\n        <RadarGrid\n          radius={radius}\n          gridColor={gridColor}\n          rings={rings}\n          showCrosshairs={showCrosshairs}\n        />\n\n        <RadarSweep\n          instanceId={instanceId}\n          radius={radius}\n          color={color}\n          duration={duration}\n        />\n\n        <circle cx={radius} cy={radius} r={4} fill={color} />\n\n        <circle cx={radius} cy={radius} r={radius - 2} fill=\"none\" stroke={color} strokeWidth={2} />\n      </svg>\n    </div>\n  )\n}\n\nconst RadarGrid = ({ radius, gridColor, rings, showCrosshairs }: RadarGridProps) => {\n  const gridRadius = radius - 2\n\n  return (\n    <g>\n      {Array.from({ length: rings }, (_, ringIndex) => {\n        const ringRadius = (gridRadius / rings) * (ringIndex + 1)\n        return (\n          <circle\n            key={ringIndex}\n            cx={radius}\n            cy={radius}\n            r={ringRadius}\n            fill=\"none\"\n            stroke={gridColor}\n            strokeWidth={1}\n          />\n        )\n      })}\n\n      {showCrosshairs && (\n        <>\n          <line\n            x1={radius - gridRadius}\n            y1={radius}\n            x2={radius + gridRadius}\n            y2={radius}\n            stroke={gridColor}\n            strokeWidth={1}\n          />\n          <line\n            x1={radius}\n            y1={radius - gridRadius}\n            x2={radius}\n            y2={radius + gridRadius}\n            stroke={gridColor}\n            strokeWidth={1}\n          />\n        </>\n      )}\n    </g>\n  )\n}\n\nconst RadarSweep = ({ instanceId, radius, color, duration }: RadarSweepProps) => {\n  const sweepRadius = radius - 2\n  const rgb = parseRgba(color)\n  const clipId = `radar-clip-${instanceId}`\n\n  return (\n    <g clipPath={`url(#${clipId})`}>\n      <g\n        style={{\n          transformOrigin: `${radius}px ${radius}px`,\n          animation: `radar-sweep ${duration}ms linear infinite`,\n        }}\n      >\n        <SweepGradient radius={radius} sweepRadius={sweepRadius} rgb={rgb} />\n\n        <line\n          x1={radius}\n          y1={radius}\n          x2={radius}\n          y2={radius - sweepRadius}\n          stroke={color}\n          strokeWidth={2}\n        />\n      </g>\n    </g>\n  )\n}\n\nconst SweepGradient = ({ radius, sweepRadius, rgb }: SweepGradientProps) => {\n  return (\n    <>\n      {Array.from({ length: SWEEP_SEGMENT_COUNT }, (_, segmentIndex) => {\n        return (\n          <SweepSegment\n            key={segmentIndex}\n            radius={radius}\n            sweepRadius={sweepRadius}\n            rgb={rgb}\n            segmentIndex={segmentIndex}\n          />\n        )\n      })}\n    </>\n  )\n}\n\nconst SweepSegment = ({ radius, sweepRadius, rgb, segmentIndex }: SweepSegmentProps) => {\n  const segmentArc = SWEEP_ARC / SWEEP_SEGMENT_COUNT\n  const startAngle = -Math.PI / 2 - SWEEP_ARC + segmentArc * segmentIndex\n  const endAngle = startAngle + segmentArc\n  const opacity = (segmentIndex / SWEEP_SEGMENT_COUNT) * 0.4\n\n  const startX = radius + sweepRadius * Math.cos(startAngle)\n  const startY = radius + sweepRadius * Math.sin(startAngle)\n  const endX = radius + sweepRadius * Math.cos(endAngle)\n  const endY = radius + sweepRadius * Math.sin(endAngle)\n\n  const largeArc = segmentArc > Math.PI ? 1 : 0\n\n  return (\n    <path\n      d={`M ${radius} ${radius} L ${startX} ${startY} A ${sweepRadius} ${sweepRadius} 0 ${largeArc} 1 ${endX} ${endY} Z`}\n      fill={`rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`}\n    />\n  )\n}\n\nconst parseRgba = (color: string): RgbColor => {\n  const match = color.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/)\n  if (!match) {\n    return { r: 0, g: 255, b: 70 }\n  }\n\n  return { r: parseInt(match[1], 10), g: parseInt(match[2], 10), b: parseInt(match[3], 10) }\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/radar.tsx"
    }
  ],
  "type": "registry:ui"
}
