{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "targeting-reticle",
  "title": "Map Targeting Reticle",
  "description": "Military-style targeting brackets with tracking and lock-on capabilities.",
  "dependencies": ["mapbox-gl"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/targeting-reticle.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useCallback, useState, type CSSProperties } from \"react\"\nimport { useMap } from \"./hooks\"\nimport type { MapCoordinates } from \"./types\"\n\nconst DEFAULT_SIZE = 120\nconst DEFAULT_BRACKET_LENGTH = 24\nconst DEFAULT_BRACKET_THICKNESS = 2\nconst DEFAULT_GAP = 8\nconst DEFAULT_COLOR = \"rgba(59, 130, 246, 0.9)\"\nconst DEFAULT_LOCKED_COLOR = \"rgba(34, 197, 94, 0.9)\"\nconst DEFAULT_TRACKING_SPEED = 0.02\nconst DEFAULT_LOCKED_THICKNESS_MULTIPLIER = 2.5\nconst DEFAULT_CROSSHAIR_LENGTH = 12\n\ntype MapTargetingReticleProps = {\n  coordinates: MapCoordinates\n  target?: MapCoordinates\n  size?: number\n  bracketLength?: number\n  bracketThickness?: number\n  gap?: number\n  color?: string\n  lockedColor?: string\n  locked?: boolean\n  trackingSpeed?: number\n  showCrosshair?: boolean\n  showCoordinates?: boolean\n  onLocked?: () => void\n}\n\ntype Corner = \"topLeft\" | \"topRight\" | \"bottomLeft\" | \"bottomRight\"\n\ntype CornerBracketProps = {\n  corner: Corner\n  bracketLength: number\n  bracketThickness: number\n  gap: number\n  color: string\n}\n\nconst CornerBracket = ({ corner, bracketLength, bracketThickness, gap, color }: CornerBracketProps) => {\n  const getPosition = (): CSSProperties => {\n    switch (corner) {\n      case \"topLeft\":\n        return { top: gap, left: gap }\n      case \"topRight\":\n        return { top: gap, right: gap }\n      case \"bottomLeft\":\n        return { bottom: gap, left: gap }\n      case \"bottomRight\":\n        return { bottom: gap, right: gap }\n    }\n  }\n\n  const getTransform = (): string => {\n    switch (corner) {\n      case \"topLeft\":\n        return \"rotate(0deg)\"\n      case \"topRight\":\n        return \"rotate(90deg)\"\n      case \"bottomLeft\":\n        return \"rotate(-90deg)\"\n      case \"bottomRight\":\n        return \"rotate(180deg)\"\n    }\n  }\n\n  const style: CSSProperties = {\n    position: \"absolute\",\n    width: bracketLength,\n    height: bracketLength,\n    ...getPosition(),\n  }\n\n  return (\n    <div style={style}>\n      <svg\n        width={bracketLength}\n        height={bracketLength}\n        viewBox={`0 0 ${bracketLength} ${bracketLength}`}\n        style={{ transform: getTransform() }}\n      >\n        <path\n          d={`M 0 ${bracketLength} L 0 0 L ${bracketLength} 0`}\n          fill=\"none\"\n          stroke={color}\n          strokeWidth={bracketThickness}\n          strokeLinecap=\"square\"\n        />\n      </svg>\n    </div>\n  )\n}\n\ntype CrosshairProps = {\n  size: number\n  color: string\n  thickness: number\n}\n\nconst Crosshair = ({ size, color, thickness }: CrosshairProps) => {\n  const center = size / 2\n\n  const style: CSSProperties = {\n    position: \"absolute\",\n    top: 0,\n    left: 0,\n    width: size,\n    height: size,\n    pointerEvents: \"none\",\n  }\n\n  return (\n    <svg style={style} viewBox={`0 0 ${size} ${size}`}>\n      <line\n        x1={center - DEFAULT_CROSSHAIR_LENGTH / 2}\n        y1={center}\n        x2={center + DEFAULT_CROSSHAIR_LENGTH / 2}\n        y2={center}\n        stroke={color}\n        strokeWidth={thickness}\n      />\n      <line\n        x1={center}\n        y1={center - DEFAULT_CROSSHAIR_LENGTH / 2}\n        x2={center}\n        y2={center + DEFAULT_CROSSHAIR_LENGTH / 2}\n        stroke={color}\n        strokeWidth={thickness}\n      />\n    </svg>\n  )\n}\n\ntype CoordinatesDisplayProps = {\n  coordinates: MapCoordinates\n  size: number\n  color: string\n}\n\nconst getDirection = (value: number, isLatitude: boolean): string => {\n  if (isLatitude) {\n    return value >= 0 ? \"N\" : \"S\"\n  }\n\n  return value >= 0 ? \"E\" : \"W\"\n}\n\nconst formatCoordinate = (value: number, isLatitude: boolean): string => {\n  const absolute = Math.abs(value)\n  const degrees = Math.floor(absolute)\n  const minutes = Math.floor((absolute - degrees) * 60)\n  const seconds = ((absolute - degrees - minutes / 60) * 3600).toFixed(1)\n  const direction = getDirection(value, isLatitude)\n\n  return `${degrees}°${minutes}'${seconds}\"${direction}`\n}\n\nconst CoordinatesDisplay = ({ coordinates, size, color }: CoordinatesDisplayProps) => {\n  const [lng, lat] = coordinates\n\n  const style: CSSProperties = {\n    position: \"absolute\",\n    top: size + 4,\n    left: \"50%\",\n    transform: \"translateX(-50%)\",\n    whiteSpace: \"nowrap\",\n    fontFamily: \"ui-monospace, monospace\",\n    fontSize: \"10px\",\n    fontWeight: 600,\n    color: color,\n    textShadow: \"0 1px 2px rgba(0,0,0,0.8)\",\n    letterSpacing: \"0.5px\",\n  }\n\n  return (\n    <div style={style}>\n      {formatCoordinate(lat, true)} {formatCoordinate(lng, false)}\n    </div>\n  )\n}\n\nexport const MapTargetingReticle = ({\n  coordinates,\n  target,\n  size = DEFAULT_SIZE,\n  bracketLength = DEFAULT_BRACKET_LENGTH,\n  bracketThickness = DEFAULT_BRACKET_THICKNESS,\n  gap = DEFAULT_GAP,\n  color = DEFAULT_COLOR,\n  lockedColor = DEFAULT_LOCKED_COLOR,\n  locked = false,\n  trackingSpeed = DEFAULT_TRACKING_SPEED,\n  showCrosshair = true,\n  showCoordinates = false,\n  onLocked,\n}: MapTargetingReticleProps) => {\n  const { map, isLoaded } = useMap()\n  const overlayRef = useRef<HTMLDivElement | null>(null)\n  const animationFrameRef = useRef<number | null>(null)\n  const currentPositionRef = useRef<{ x: number; y: number } | null>(null)\n  const [isLocked, setIsLocked] = useState(locked)\n  const [currentCoordinates, setCurrentCoordinates] = useState<MapCoordinates>(coordinates)\n\n  const targetRef = useRef(target)\n  const coordinatesRef = useRef(coordinates)\n  const onLockedRef = useRef(onLocked)\n  const isLockedRef = useRef(isLocked)\n\n  targetRef.current = target\n  coordinatesRef.current = coordinates\n  onLockedRef.current = onLocked\n  isLockedRef.current = isLocked\n\n  const activeColor = isLocked ? lockedColor : color\n  const activeThickness = isLocked ? bracketThickness * DEFAULT_LOCKED_THICKNESS_MULTIPLIER : bracketThickness\n\n  const updatePosition = useCallback(() => {\n    if (!overlayRef.current || !map) {\n      return\n    }\n\n    const currentTarget = targetRef.current\n    const currentCoords = coordinatesRef.current\n    const positionToUse = isLockedRef.current && currentTarget ? currentTarget : currentCoords\n    const projected = map.project(positionToUse)\n    const halfSize = size / 2\n\n    overlayRef.current.style.left = `${projected.x - halfSize}px`\n    overlayRef.current.style.top = `${projected.y - halfSize}px`\n  }, [map, size])\n\n  const animateToTarget = useCallback((): boolean => {\n    const currentTarget = targetRef.current\n\n    if (!map || !currentTarget || isLockedRef.current) {\n      return false\n    }\n\n    const targetProjected = map.project(currentTarget)\n\n    if (!currentPositionRef.current) {\n      const currentProjected = map.project(coordinatesRef.current)\n      currentPositionRef.current = { x: currentProjected.x, y: currentProjected.y }\n    }\n\n    const current = currentPositionRef.current\n    const dx = targetProjected.x - current.x\n    const dy = targetProjected.y - current.y\n    const distance = Math.sqrt(dx * dx + dy * dy)\n\n    if (distance < 2) {\n      setIsLocked(true)\n      setCurrentCoordinates(currentTarget)\n      onLockedRef.current?.()\n      return false\n    }\n\n    current.x += dx * trackingSpeed\n    current.y += dy * trackingSpeed\n\n    if (overlayRef.current) {\n      const halfSize = size / 2\n      overlayRef.current.style.left = `${current.x - halfSize}px`\n      overlayRef.current.style.top = `${current.y - halfSize}px`\n    }\n\n    if (showCoordinates) {\n      const lngLat = map.unproject([current.x, current.y])\n      setCurrentCoordinates([lngLat.lng, lngLat.lat])\n    }\n\n    return true\n  }, [map, trackingSpeed, size, showCoordinates])\n\n  useEffect(() => {\n    setIsLocked(locked)\n    if (locked && target) {\n      setCurrentCoordinates(target)\n    }\n  }, [locked, target])\n\n  useEffect(() => {\n    if (!map || !isLoaded) {\n      return\n    }\n\n    if (target && !isLocked) {\n      if (!currentPositionRef.current) {\n        const currentProjected = map.project(coordinates)\n        currentPositionRef.current = { x: currentProjected.x, y: currentProjected.y }\n      }\n      const runAnimation = () => {\n        const shouldContinue = animateToTarget()\n        if (shouldContinue) {\n          animationFrameRef.current = requestAnimationFrame(runAnimation)\n        }\n      }\n      runAnimation()\n    } else {\n      updatePosition()\n    }\n\n    const handleMapChange = () => {\n      if (!targetRef.current || isLockedRef.current) {\n        updatePosition()\n      }\n    }\n\n    map.on(\"move\", handleMapChange)\n    map.on(\"zoom\", handleMapChange)\n    map.on(\"rotate\", handleMapChange)\n    map.on(\"pitch\", handleMapChange)\n    map.on(\"resize\", handleMapChange)\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n      }\n      map.off(\"move\", handleMapChange)\n      map.off(\"zoom\", handleMapChange)\n      map.off(\"rotate\", handleMapChange)\n      map.off(\"pitch\", handleMapChange)\n      map.off(\"resize\", handleMapChange)\n    }\n  }, [map, isLoaded, target, isLocked, coordinates, updatePosition, animateToTarget])\n\n  const containerStyle: CSSProperties = {\n    position: \"absolute\",\n    width: size,\n    height: size,\n    pointerEvents: \"none\",\n    zIndex: 10,\n    transition: isLocked ? \"none\" : \"opacity 0.2s ease-out\",\n  }\n\n  const corners: Corner[] = [\"topLeft\", \"topRight\", \"bottomLeft\", \"bottomRight\"]\n\n  return (\n    <div ref={overlayRef} style={containerStyle}>\n      {corners.map((corner) => {\n        return (\n          <CornerBracket\n            key={corner}\n            corner={corner}\n            bracketLength={bracketLength}\n            bracketThickness={activeThickness}\n            gap={gap}\n            color={activeColor}\n          />\n        )\n      })}\n      {showCrosshair && <Crosshair size={size} color={activeColor} thickness={activeThickness} />}\n      {showCoordinates && <CoordinatesDisplay coordinates={currentCoordinates} size={size} color={activeColor} />}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/targeting-reticle.tsx"
    }
  ],
  "type": "registry:ui"
}
