{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "controls",
  "title": "Map Controls",
  "description": "Interactive map controls (zoom, fullscreen, etc).",
  "dependencies": ["mapbox-gl", "lucide-react"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/controls.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\"\nimport { Minus, Plus, Locate, Maximize, Loader2 } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { useMap } from \"./hooks\"\n\ntype MapControlPosition = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\"\n\ntype MapControlsProps = {\n  children: ReactNode\n  position?: MapControlPosition\n  className?: string\n}\n\ntype ControlButtonProps = {\n  onClick: () => void\n  label: string\n  children: ReactNode\n  disabled?: boolean\n}\n\ntype MapGeolocateProps = {\n  onLocate?: (coords: GeolocationCoordinates) => void\n}\n\ntype GeolocationCoordinates = {\n  longitude: number\n  latitude: number\n}\n\nconst DEFAULT_POSITION: MapControlPosition = \"bottom-right\"\nconst ZOOM_STEP = 1\nconst ZOOM_DURATION = 300\nconst GEOLOCATE_ZOOM = 14\nconst GEOLOCATE_DURATION = 1500\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}\nconst DEFAULT_DISABLED = false\nconst DEFAULT_GEOLOCATE_PROPS: MapGeolocateProps = {}\n\nconst ControlGroup = ({ children }: { children: ReactNode }) => {\n  return (\n    <div className=\"flex flex-col rounded-md border border-border bg-background shadow-sm overflow-hidden [&>button:not(:last-child)]:border-b [&>button:not(:last-child)]:border-border\">\n      {children}\n    </div>\n  )\n}\n\nconst ControlButton = ({ onClick, label, children, disabled = DEFAULT_DISABLED }: ControlButtonProps) => {\n  return (\n    <button\n      onClick={onClick}\n      aria-label={label}\n      type=\"button\"\n      className={cn(\n        \"flex items-center justify-center size-10 md:size-8 hover:bg-accent dark:hover:bg-accent/40 transition-colors\",\n        disabled && \"opacity-50 pointer-events-none cursor-not-allowed\"\n      )}\n      disabled={disabled}\n    >\n      {children}\n    </button>\n  )\n}\n\nexport const MapControls = ({ children, position = DEFAULT_POSITION, className }: MapControlsProps) => {\n  const { isLoaded } = useMap()\n\n  if (!isLoaded) {\n    return null\n  }\n\n  return (\n    <div className={cn(\"absolute z-10 flex flex-col gap-1.5\", POSITION_CLASSES[position], className)}>{children}</div>\n  )\n}\n\nexport const MapZoom = () => {\n  const { map } = useMap()\n\n  const handleZoomIn = () => {\n    map?.zoomTo(map.getZoom() + ZOOM_STEP, { duration: ZOOM_DURATION })\n  }\n\n  const handleZoomOut = () => {\n    map?.zoomTo(map.getZoom() - ZOOM_STEP, { duration: ZOOM_DURATION })\n  }\n\n  return (\n    <ControlGroup>\n      <ControlButton onClick={handleZoomIn} label=\"Zoom in\">\n        <Plus className=\"size-4\" />\n      </ControlButton>\n      <ControlButton onClick={handleZoomOut} label=\"Zoom out\">\n        <Minus className=\"size-4\" />\n      </ControlButton>\n    </ControlGroup>\n  )\n}\n\nexport const MapOrientation = () => {\n  const { map, isLoaded } = useMap()\n  const compassRef = useRef<SVGSVGElement>(null)\n\n  useEffect(() => {\n    if (!isLoaded || !map || !compassRef.current) {\n      return\n    }\n\n    const compass = compassRef.current\n\n    const updateRotation = () => {\n      const bearing = map.getBearing()\n      const pitch = map.getPitch()\n      compass.style.transform = `rotateX(${pitch}deg) rotateZ(${-bearing}deg)`\n    }\n\n    map.on(\"rotate\", updateRotation)\n    map.on(\"pitch\", updateRotation)\n    updateRotation()\n\n    return () => {\n      map.off(\"rotate\", updateRotation)\n      map.off(\"pitch\", updateRotation)\n    }\n  }, [isLoaded, map])\n\n  const handleResetBearing = () => {\n    map?.resetNorthPitch({ duration: ZOOM_DURATION })\n  }\n\n  return (\n    <ControlGroup>\n      <ControlButton onClick={handleResetBearing} label=\"Reset bearing to north\">\n        <svg\n          ref={compassRef}\n          viewBox=\"0 0 24 24\"\n          className=\"size-5 transition-transform duration-200\"\n          style={{ transformStyle: \"preserve-3d\" }}\n        >\n          <path d=\"M12 2L16 12H12V2Z\" className=\"fill-red-500\" />\n          <path d=\"M12 2L8 12H12V2Z\" className=\"fill-red-300\" />\n          <path d=\"M12 22L16 12H12V22Z\" className=\"fill-muted-foreground/60\" />\n          <path d=\"M12 22L8 12H12V22Z\" className=\"fill-muted-foreground/30\" />\n        </svg>\n      </ControlButton>\n    </ControlGroup>\n  )\n}\n\nexport const MapGeolocate = ({ onLocate }: MapGeolocateProps = DEFAULT_GEOLOCATE_PROPS) => {\n  const { map } = useMap()\n  const [isLocating, setIsLocating] = useState(false)\n\n  const handleLocate = () => {\n    setIsLocating(true)\n    if (\"geolocation\" in navigator) {\n      navigator.geolocation.getCurrentPosition(\n        (pos) => {\n          const coords: GeolocationCoordinates = {\n            longitude: pos.coords.longitude,\n            latitude: pos.coords.latitude,\n          }\n          map?.flyTo({\n            center: [coords.longitude, coords.latitude],\n            zoom: GEOLOCATE_ZOOM,\n            duration: GEOLOCATE_DURATION,\n          })\n          onLocate?.(coords)\n          setIsLocating(false)\n        },\n        (error) => {\n          console.error(\"Error getting location:\", error)\n          setIsLocating(false)\n        }\n      )\n    }\n  }\n\n  return (\n    <ControlGroup>\n      <ControlButton onClick={handleLocate} label=\"Find my location\" disabled={isLocating}>\n        {isLocating ? <Loader2 className=\"size-4 animate-spin\" /> : <Locate className=\"size-4\" />}\n      </ControlButton>\n    </ControlGroup>\n  )\n}\n\nexport const MapFullscreen = () => {\n  const { map } = useMap()\n\n  const handleFullscreen = () => {\n    const container = map?.getContainer()\n    if (!container) {\n      return\n    }\n    if (document.fullscreenElement) {\n      document.exitFullscreen()\n    } else {\n      container.requestFullscreen()\n    }\n  }\n\n  return (\n    <ControlGroup>\n      <ControlButton onClick={handleFullscreen} label=\"Toggle fullscreen\">\n        <Maximize className=\"size-4\" />\n      </ControlButton>\n    </ControlGroup>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/controls.tsx"
    }
  ],
  "type": "registry:ui"
}
