{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "map-view",
  "title": "Map View",
  "description": "A theme-aware Mapbox map wrapper with UUI-styled zoom and basemap-style controls.",
  "dependencies": [
    "@untitledui/icons@^0.0.22",
    "mapbox-gl@^3.25.0",
    "next-themes@^0.4.6",
    "react-map-gl@^8.1.1"
  ],
  "registryDependencies": [
    "@nx-ui/button",
    "@nx-ui/button-group",
    "@nx-ui/mapbox",
    "@nx-ui/mapbox-overrides",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/maps/map-view.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Globe01, Map01, Minus, Plus } from \"@untitledui/icons\"\nimport { useTheme } from \"next-themes\"\nimport MapboxMap, {\n  type MapProps,\n  type MapRef,\n  useMap,\n} from \"react-map-gl/mapbox\"\nimport \"mapbox-gl/dist/mapbox-gl.css\"\n// Hides Mapbox's injected logo/attribution chrome on every map.\n// @registry-dep: mapbox-overrides\nimport \"@/styles/mapbox-overrides.css\"\n\nimport { MAPBOX_STYLES, MAPBOX_TOKEN } from \"@/lib/mapbox\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { ButtonGroup } from \"@/components/ui/button-group\"\n\n/**\n * map-view — the foundation Mapbox block for nx-ui.\n *\n * A thin, theme-aware wrapper around react-map-gl's <Map> that gives every\n * other map block (site-map, route-map, boundary-map…) a consistent frame and\n * a set of UUI-styled controls:\n *\n *   • The container is a `rounded-xl border-border overflow-hidden` surface, so\n *     maps sit in the gallery like any other nx-ui card.\n *   • The basemap style follows next-themes: the custom Nexus/SoilLink light\n *     style in light mode, Mapbox dark-v11 in dark mode, and it live-switches on\n *     theme toggle. A Map/Satellite switcher lets the user override to the\n *     satellite-streets style.\n *   • Zoom in/out render as our own ButtonGroup, not Mapbox's default controls.\n *\n * It is a controlled-or-uncontrolled <Map> pass-through: any react-map-gl\n * MapProps (initialViewState, onLoad, children Sources/Layers/Markers…) flow\n * straight through, so consumers compose Source/Layer/Marker as usual.\n */\n\n/** The three basemap modes exposed by the built-in style switcher. */\nexport type MapBasemap = \"map\" | \"satellite\"\n\n/** Sensible UK-wide default frame (centre of GB), used when no view is given. */\nexport const UK_DEFAULT_VIEW = {\n  longitude: -2.2,\n  latitude: 54.0,\n  zoom: 5,\n} as const\n\n/** Resolve the Mapbox style URL for the active basemap + theme. */\nexport function resolveMapStyle(\n  basemap: MapBasemap,\n  resolvedTheme: string | undefined\n): string {\n  if (basemap === \"satellite\") return MAPBOX_STYLES.satellite\n  return resolvedTheme === \"dark\" ? MAPBOX_STYLES.dark : MAPBOX_STYLES.light\n}\n\n/** UUI zoom in/out control, rendered as our ButtonGroup. Uses the react-map-gl\n * map context so it works for any <MapView> child without prop drilling — it\n * must render INSIDE the <Map> (as a child) for `useMap()` to resolve; mounted\n * outside, the buttons would be silent no-ops. */\nexport function MapZoomControl({ className }: { className?: string }) {\n  const { current: map } = useMap()\n  return (\n    <ButtonGroup\n      orientation=\"vertical\"\n      className={cn(\"shadow-lg\", className)}\n      aria-label=\"Zoom\"\n    >\n      <Button\n        type=\"button\"\n        variant=\"secondary\"\n        size=\"icon-sm\"\n        aria-label=\"Zoom in\"\n        onClick={() => map?.zoomIn({ duration: 300 })}\n      >\n        <Plus />\n      </Button>\n      <Button\n        type=\"button\"\n        variant=\"secondary\"\n        size=\"icon-sm\"\n        aria-label=\"Zoom out\"\n        onClick={() => map?.zoomOut({ duration: 300 })}\n      >\n        <Minus />\n      </Button>\n    </ButtonGroup>\n  )\n}\n\n/** Map / Satellite basemap switcher, styled as our ButtonGroup (nexus4 pattern). */\nexport function MapStyleSwitcher({\n  basemap,\n  onBasemapChange,\n  className,\n}: {\n  basemap: MapBasemap\n  onBasemapChange: (basemap: MapBasemap) => void\n  className?: string\n}) {\n  return (\n    <ButtonGroup className={cn(\"shadow-lg\", className)} aria-label=\"Basemap style\">\n      <Button\n        type=\"button\"\n        variant={basemap === \"map\" ? \"default\" : \"secondary\"}\n        size=\"sm\"\n        aria-pressed={basemap === \"map\"}\n        onClick={() => onBasemapChange(\"map\")}\n      >\n        <Map01 />\n        Map\n      </Button>\n      <Button\n        type=\"button\"\n        variant={basemap === \"satellite\" ? \"default\" : \"secondary\"}\n        size=\"sm\"\n        aria-pressed={basemap === \"satellite\"}\n        onClick={() => onBasemapChange(\"satellite\")}\n      >\n        <Globe01 />\n        Satellite\n      </Button>\n    </ButtonGroup>\n  )\n}\n\nexport interface MapViewProps extends Omit<MapProps, \"mapboxAccessToken\" | \"mapStyle\"> {\n  /** Override the resolved Mapbox style URL entirely. */\n  mapStyle?: string\n  /** Show the built-in UUI zoom control (top-right). Default true. */\n  showZoom?: boolean\n  /** Show the built-in Map/Satellite style switcher (top-left). Default true. */\n  showStyleSwitcher?: boolean\n  /** Starting basemap for the style switcher. Default \"map\". */\n  defaultBasemap?: MapBasemap\n  /** Extra classes for the rounded frame container. */\n  className?: string\n  /** Extra content rendered as an overlay above the map (e.g. stat panels). */\n  overlay?: React.ReactNode\n  children?: React.ReactNode\n}\n\n/**\n * MapView — the map frame + Mapbox <Map> + UUI controls.\n * Forwards a MapRef so callers can drive the camera (fitBounds, flyTo…).\n */\nexport const MapView = React.forwardRef<MapRef, MapViewProps>(function MapView(\n  {\n    showZoom = true,\n    showStyleSwitcher = true,\n    defaultBasemap = \"map\",\n    mapStyle,\n    className,\n    overlay,\n    children,\n    initialViewState,\n    style,\n    ...mapProps\n  },\n  ref\n) {\n  const { resolvedTheme } = useTheme()\n  const [basemap, setBasemap] = React.useState<MapBasemap>(defaultBasemap)\n\n  const resolvedStyle = mapStyle ?? resolveMapStyle(basemap, resolvedTheme)\n\n  return (\n    <div\n      className={cn(\n        \"relative size-full overflow-hidden rounded-xl border border-border bg-muted\",\n        className\n      )}\n    >\n      <MapboxMap\n        ref={ref}\n        mapboxAccessToken={MAPBOX_TOKEN}\n        mapStyle={resolvedStyle}\n        initialViewState={initialViewState ?? UK_DEFAULT_VIEW}\n        attributionControl={false}\n        style={{ width: \"100%\", height: \"100%\", ...style }}\n        {...mapProps}\n      >\n        {children}\n        {/* Rendered as a Map child so useMap() resolves to this map — mounted\n            outside the <Map>, the control has no context and the zoom buttons\n            are silent no-ops. The absolute wrapper still anchors to the frame. */}\n        {showZoom ? (\n          <div className=\"absolute top-3 right-3 z-10\">\n            <MapZoomControl />\n          </div>\n        ) : null}\n      </MapboxMap>\n\n      {showStyleSwitcher ? (\n        <div className=\"absolute top-3 left-3 z-10\">\n          <MapStyleSwitcher basemap={basemap} onBasemapChange={setBasemap} />\n        </div>\n      ) : null}\n\n      {overlay}\n    </div>\n  )\n})\n",
      "type": "registry:component",
      "target": "components/blocks/maps/map-view.tsx"
    }
  ],
  "type": "registry:block"
}