{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "boundary-map",
  "title": "Boundary Map",
  "description": "A site-boundary map rendering polygon parcels with a brand fill, outline and hover state.",
  "dependencies": [
    "react-map-gl@^8.1.1"
  ],
  "registryDependencies": [
    "@nx-ui/map-view",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/maps/boundary-map.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport type { Feature, FeatureCollection, Polygon } from \"geojson\"\nimport {\n  Layer,\n  type MapMouseEvent,\n  type MapRef,\n  Source,\n} from \"react-map-gl/mapbox\"\n\nimport { cn } from \"@/lib/utils\"\nimport { MapView, type MapViewProps } from \"@/components/blocks/maps/map-view\"\n\n/**\n * boundary-map — site boundary polygons for nx-ui.\n *\n * Renders one or more parcels as filled polygons: a translucent brand fill\n * (≈ brand-500/10), a solid brand-600 outline, and a hover state that lifts the\n * fill opacity and thickens the outline. Hover is driven by Mapbox feature-state\n * (`setFeatureState`), and the camera fit-bounds to all parcels on load.\n */\n\nconst BRAND_500 = \"#2A2D78\"\nconst BRAND_600 = \"#121541\"\n\nexport interface BoundaryParcel {\n  id: string\n  name?: string\n  /** Polygon rings: [ [ [lng,lat], ... ] ]. First ring is the outer boundary. */\n  rings: [number, number][][]\n}\n\nexport interface BoundaryMapProps\n  extends Omit<MapViewProps, \"children\" | \"onClick\" | \"interactiveLayerIds\"> {\n  parcels: BoundaryParcel[]\n  className?: string\n  /** Fired when a parcel is clicked. */\n  onSelectParcel?: (parcel: BoundaryParcel) => void\n}\n\nconst FILL_LAYER = \"boundary-fill\"\nconst LINE_LAYER = \"boundary-line\"\n\nfunction boundsOfParcels(\n  parcels: BoundaryParcel[]\n): [[number, number], [number, number]] | null {\n  const coords = parcels.flatMap((p) => p.rings.flat())\n  if (coords.length === 0) return null\n  let minLng = coords[0][0]\n  let maxLng = coords[0][0]\n  let minLat = coords[0][1]\n  let maxLat = coords[0][1]\n  for (const [lng, lat] of coords) {\n    minLng = Math.min(minLng, lng)\n    maxLng = Math.max(maxLng, lng)\n    minLat = Math.min(minLat, lat)\n    maxLat = Math.max(maxLat, lat)\n  }\n  return [\n    [minLng, minLat],\n    [maxLng, maxLat],\n  ]\n}\n\nexport function BoundaryMap({\n  parcels,\n  className,\n  onSelectParcel,\n  ...mapViewProps\n}: BoundaryMapProps) {\n  const mapRef = React.useRef<MapRef>(null)\n  const hoveredId = React.useRef<string | number | null>(null)\n\n  const geojson = React.useMemo<FeatureCollection<Polygon>>(\n    () => ({\n      type: \"FeatureCollection\",\n      features: parcels.map(\n        (parcel): Feature<Polygon> => ({\n          type: \"Feature\",\n          id: parcel.id,\n          geometry: { type: \"Polygon\", coordinates: parcel.rings },\n          properties: { id: parcel.id, name: parcel.name ?? \"\" },\n        })\n      ),\n    }),\n    [parcels]\n  )\n\n  const fitToParcels = React.useCallback(() => {\n    const map = mapRef.current?.getMap()\n    const bounds = boundsOfParcels(parcels)\n    if (!map || !bounds) return\n    map.fitBounds(bounds, { padding: 56, maxZoom: 15, duration: 500 })\n  }, [parcels])\n\n  React.useEffect(() => {\n    if (mapRef.current?.getMap()?.isStyleLoaded()) fitToParcels()\n  }, [fitToParcels])\n\n  const setHover = React.useCallback((id: string | number | null) => {\n    const map = mapRef.current?.getMap()\n    if (!map) return\n    if (hoveredId.current != null) {\n      map.setFeatureState(\n        { source: \"boundaries\", id: hoveredId.current },\n        { hover: false }\n      )\n    }\n    hoveredId.current = id\n    if (id != null) {\n      map.setFeatureState({ source: \"boundaries\", id }, { hover: true })\n    }\n  }, [])\n\n  const handleMouseMove = React.useCallback(\n    (event: MapMouseEvent) => {\n      const feature = event.features?.[0]\n      const map = mapRef.current?.getMap()\n      if (map) map.getCanvas().style.cursor = feature ? \"pointer\" : \"\"\n      setHover(feature?.id ?? null)\n    },\n    [setHover]\n  )\n\n  const handleClick = React.useCallback(\n    (event: MapMouseEvent) => {\n      const id = event.features?.[0]?.properties?.id as string | undefined\n      if (!id) return\n      const parcel = parcels.find((p) => p.id === id)\n      if (parcel) onSelectParcel?.(parcel)\n    },\n    [parcels, onSelectParcel]\n  )\n\n  return (\n    <MapView\n      ref={mapRef}\n      className={cn(className)}\n      onLoad={fitToParcels}\n      interactiveLayerIds={[FILL_LAYER]}\n      onMouseMove={handleMouseMove}\n      onMouseLeave={() => setHover(null)}\n      onClick={handleClick}\n      {...mapViewProps}\n    >\n      <Source id=\"boundaries\" type=\"geojson\" data={geojson}>\n        <Layer\n          id={FILL_LAYER}\n          type=\"fill\"\n          paint={{\n            \"fill-color\": BRAND_500,\n            \"fill-opacity\": [\n              \"case\",\n              [\"boolean\", [\"feature-state\", \"hover\"], false],\n              0.28,\n              0.1,\n            ],\n          }}\n        />\n        <Layer\n          id={LINE_LAYER}\n          type=\"line\"\n          paint={{\n            \"line-color\": BRAND_600,\n            \"line-width\": [\n              \"case\",\n              [\"boolean\", [\"feature-state\", \"hover\"], false],\n              3,\n              1.75,\n            ],\n          }}\n        />\n      </Source>\n    </MapView>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/maps/boundary-map.tsx"
    }
  ],
  "type": "registry:block"
}