{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "compass",
  "title": "Map Compass",
  "description": "Interactive compass overlay with bearing display and click-to-reset.",
  "dependencies": ["mapbox-gl"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/compass.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { useMap } from \"./hooks\"\n\ntype MapControlPosition = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\"\ntype MapCompassSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype MapCompassProps = {\n  size?: MapCompassSize | number\n  position?: MapControlPosition\n  showCardinals?: boolean\n  showRing?: boolean\n  showBearing?: boolean\n  autoRotate?: boolean\n  autoRotateSpeed?: number\n  className?: string\n}\n\nconst DEFAULT_SIZE: MapCompassSize = \"md\"\nconst DEFAULT_POSITION: MapControlPosition = \"top-right\"\nconst DEFAULT_SHOW_CARDINALS = true\nconst DEFAULT_SHOW_RING = true\nconst DEFAULT_SHOW_BEARING = false\nconst DEFAULT_AUTO_ROTATE = false\nconst DEFAULT_AUTO_ROTATE_SPEED = 2\n\nconst SIZE_MAP: Record<MapCompassSize, number> = {\n  sm: 48,\n  md: 64,\n  lg: 80,\n  xl: 96,\n}\n\nconst POSITION_CLASSES: Record<MapControlPosition, string> = {\n  \"top-left\": \"top-2 left-2\",\n  \"top-right\": \"top-2 right-2\",\n  \"bottom-left\": \"bottom-2 left-2\",\n  \"bottom-right\": \"bottom-10 right-2\",\n}\n\nconst resolveSize = (size: MapCompassSize | number): number => {\n  if (typeof size === \"number\") {\n    return size\n  }\n  return SIZE_MAP[size]\n}\n\nconst CompassRing = () => {\n  const majorAngles = [0, 45, 90, 135, 180, 225, 270, 315]\n  const minorAngles = [15, 30, 60, 75, 105, 120, 150, 165, 195, 210, 240, 255, 285, 300, 330, 345]\n\n  const createTick = (angle: number, isMajor: boolean) => {\n    const rad = (angle * Math.PI) / 180\n    const innerRadius = isMajor ? 40 : 42\n    const outerRadius = 46\n    const x1 = 50 + innerRadius * Math.sin(rad)\n    const y1 = 50 - innerRadius * Math.cos(rad)\n    const x2 = 50 + outerRadius * Math.sin(rad)\n    const y2 = 50 - outerRadius * Math.cos(rad)\n\n    return (\n      <line key={angle} x1={x1} y1={y1} x2={x2} y2={y2} className=\"stroke-border\" strokeWidth={isMajor ? 1.5 : 0.75} />\n    )\n  }\n\n  return (\n    <g>\n      <circle cx=\"50\" cy=\"50\" r=\"46\" fill=\"none\" className=\"stroke-border\" strokeWidth=\"1\" />\n      {majorAngles.map((angle) => {\n        return createTick(angle, true)\n      })}\n      {minorAngles.map((angle) => {\n        return createTick(angle, false)\n      })}\n    </g>\n  )\n}\n\nconst CompassRose = () => {\n  return (\n    <g>\n      <path d=\"M50 14 L56 50 L50 46 Z\" className=\"fill-red-500\" />\n      <path d=\"M50 14 L44 50 L50 46 Z\" className=\"fill-red-400\" />\n\n      <path d=\"M50 86 L56 50 L50 54 Z\" className=\"fill-muted-foreground/50\" />\n      <path d=\"M50 86 L44 50 L50 54 Z\" className=\"fill-muted-foreground/30\" />\n\n      <path d=\"M86 50 L50 44 L54 50 Z\" className=\"fill-muted-foreground/30\" />\n      <path d=\"M86 50 L50 56 L54 50 Z\" className=\"fill-muted-foreground/20\" />\n\n      <path d=\"M14 50 L50 44 L46 50 Z\" className=\"fill-muted-foreground/30\" />\n      <path d=\"M14 50 L50 56 L46 50 Z\" className=\"fill-muted-foreground/20\" />\n    </g>\n  )\n}\n\ntype CardinalLabelsProps = {\n  fontSize: number\n}\n\nconst CardinalLabels = ({ fontSize }: CardinalLabelsProps) => {\n  return (\n    <g className=\"font-sans font-bold\" style={{ fontSize }}>\n      <text x=\"50\" y=\"28\" textAnchor=\"middle\" dominantBaseline=\"middle\" className=\"fill-red-500\">\n        N\n      </text>\n      <text x=\"50\" y=\"76\" textAnchor=\"middle\" dominantBaseline=\"middle\" className=\"fill-muted-foreground\">\n        S\n      </text>\n      <text x=\"76\" y=\"52\" textAnchor=\"middle\" dominantBaseline=\"middle\" className=\"fill-muted-foreground\">\n        E\n      </text>\n      <text x=\"24\" y=\"52\" textAnchor=\"middle\" dominantBaseline=\"middle\" className=\"fill-muted-foreground\">\n        W\n      </text>\n    </g>\n  )\n}\n\nconst CenterPivot = () => {\n  return (\n    <g>\n      <circle cx=\"50\" cy=\"50\" r=\"4\" className=\"fill-background stroke-border\" strokeWidth=\"1\" />\n      <circle cx=\"50\" cy=\"50\" r=\"2\" className=\"fill-muted-foreground/60\" />\n    </g>\n  )\n}\n\nexport const MapCompass = ({\n  size = DEFAULT_SIZE,\n  position = DEFAULT_POSITION,\n  showCardinals = DEFAULT_SHOW_CARDINALS,\n  showRing = DEFAULT_SHOW_RING,\n  showBearing = DEFAULT_SHOW_BEARING,\n  autoRotate = DEFAULT_AUTO_ROTATE,\n  autoRotateSpeed = DEFAULT_AUTO_ROTATE_SPEED,\n  className,\n}: MapCompassProps) => {\n  const { map, isLoaded } = useMap()\n  const compassRef = useRef<HTMLDivElement | null>(null)\n  const animationRef = useRef<number | null>(null)\n  const [rotation, setRotation] = useState(0)\n  const [isDragging, setIsDragging] = useState(false)\n  const [startAngle, setStartAngle] = useState(0)\n\n  const calculateAngle = (event: React.MouseEvent | MouseEvent) => {\n    const compass = compassRef.current\n    if (!compass) {\n      return 0\n    }\n\n    const rect = compass.getBoundingClientRect()\n    const centerX = rect.left + rect.width / 2\n    const centerY = rect.top + rect.height / 2\n\n    const mouseX = event.clientX - centerX\n    const mouseY = event.clientY - centerY\n\n    let angle = Math.atan2(mouseY, mouseX) * (180 / Math.PI)\n    angle = angle + 90\n\n    if (angle < 0) {\n      angle += 360\n    }\n\n    return angle\n  }\n\n  const handleMouseDown = (event: React.MouseEvent | MouseEvent) => {\n    event.preventDefault()\n    setIsDragging(true)\n\n    const angle = calculateAngle(event)\n    setStartAngle(angle - rotation)\n  }\n\n  const handleMouseMove = (event: React.MouseEvent | MouseEvent) => {\n    if (!isDragging) {\n      return\n    }\n\n    const currentAngle = calculateAngle(event)\n    let newRotation = currentAngle - startAngle\n\n    if (newRotation < 0) {\n      newRotation += 360\n    }\n    if (newRotation >= 360) {\n      newRotation -= 360\n    }\n\n    setRotation(newRotation)\n\n    if (map) {\n      map.setBearing(-newRotation)\n    }\n  }\n\n  const handleMouseUp = () => {\n    setIsDragging(false)\n  }\n\n  useEffect(() => {\n    if (isDragging) {\n      window.addEventListener(\"mousemove\", handleMouseMove)\n      window.addEventListener(\"mouseup\", handleMouseUp)\n    }\n\n    return () => {\n      window.removeEventListener(\"mousemove\", handleMouseMove)\n      window.removeEventListener(\"mouseup\", handleMouseUp)\n    }\n  }, [isDragging, startAngle, rotation])\n\n  useEffect(() => {\n    if (!autoRotate || isDragging) {\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current)\n        animationRef.current = null\n      }\n      return\n    }\n\n    const animate = () => {\n      setRotation((prev) => {\n        let newRotation = prev + autoRotateSpeed\n        if (newRotation >= 360) {\n          newRotation -= 360\n        }\n        if (newRotation < 0) {\n          newRotation += 360\n        }\n\n        if (map) {\n          map.setBearing(-newRotation)\n        }\n\n        return newRotation\n      })\n\n      animationRef.current = requestAnimationFrame(animate)\n    }\n\n    animationRef.current = requestAnimationFrame(animate)\n\n    return () => {\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current)\n      }\n    }\n  }, [autoRotate, autoRotateSpeed, isDragging, map])\n\n  if (!isLoaded) {\n    return null\n  }\n\n  const pixelSize = resolveSize(size)\n  const fontSize = Math.max(8, pixelSize * 0.14)\n\n  return (\n    <div className={cn(\"absolute z-10\", POSITION_CLASSES[position], className)}>\n      <div\n        ref={compassRef}\n        className={cn(\n          \"rounded-full bg-background/90 backdrop-blur-sm shadow-lg border border-border p-1 select-none\",\n          !isDragging && \"hover:bg-accent/20 transition-colors\"\n        )}\n        style={{ cursor: isDragging ? \"grabbing\" : \"grab\" }}\n        onMouseDown={handleMouseDown}\n      >\n        <svg\n          viewBox=\"0 0 100 100\"\n          width={pixelSize}\n          height={pixelSize}\n          className=\"pointer-events-none\"\n          style={{ transform: `rotate(${rotation}deg)` }}\n        >\n          {showRing && <CompassRing />}\n          <CompassRose />\n          {showCardinals && <CardinalLabels fontSize={fontSize} />}\n          <CenterPivot />\n        </svg>\n      </div>\n      {showBearing && (\n        <div className=\"absolute -bottom-6 left-1/2 -translate-x-1/2 text-xs font-mono text-muted-foreground whitespace-nowrap\">\n          {Math.round(rotation)}°\n        </div>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/compass.tsx"
    }
  ],
  "type": "registry:ui"
}
