{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "grid",
  "title": "Map Grid",
  "description": "Coordinate grid overlay with latitude/longitude lines and labels.",
  "dependencies": ["mapbox-gl"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/grid.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { createPortal } from \"react-dom\"\nimport { useMap } from \"./hooks\"\n\ntype GridLabel = {\n  id: string\n  text: string\n  position: { x: number; y: number }\n  type: \"lat\" | \"lng\"\n}\n\ntype GridLinesResult = {\n  latitudeLines: GeoJSON.Feature<GeoJSON.LineString>[]\n  longitudeLines: GeoJSON.Feature<GeoJSON.LineString>[]\n  labels: GridLabel[]\n}\n\ntype MapGridProps = {\n  id?: string\n  latitudeInterval?: number\n  longitudeInterval?: number\n  lineColor?: string\n  lineOpacity?: number\n  lineWidth?: number\n  showLabels?: boolean\n  labelColor?: string\n  labelSize?: number\n  labelBackground?: string\n}\n\nconst DEFAULT_ID = \"map-grid\"\nconst DEFAULT_LATITUDE_INTERVAL = 10\nconst DEFAULT_LONGITUDE_INTERVAL = 10\nconst DEFAULT_LINE_COLOR = \"#ffffff\"\nconst DEFAULT_LINE_OPACITY = 0.3\nconst DEFAULT_LINE_WIDTH = 1\nconst DEFAULT_LABEL_COLOR = \"#ffffff\"\nconst DEFAULT_LABEL_SIZE = 10\nconst DEFAULT_LABEL_BACKGROUND = \"rgba(0, 0, 0, 0.5)\"\n\nconst MIN_LATITUDE = -90\nconst MAX_LATITUDE = 90\nconst MIN_RENDER_LATITUDE = -85\nconst MAX_RENDER_LATITUDE = 85\nconst LONGITUDE_WRAP = 180\nconst LONGITUDE_CYCLE = 360\n\nconst LABEL_OFFSET_X = 8\nconst LABEL_OFFSET_Y = 16\nconst LABEL_PADDING = \"2px 4px\"\nconst LABEL_BORDER_RADIUS = \"2px\"\n\nconst createLatitudeFeature = (latitude: number, west: number, east: number): GeoJSON.Feature<GeoJSON.LineString> => {\n  return {\n    type: \"Feature\",\n    properties: { latitude },\n    geometry: {\n      type: \"LineString\",\n      coordinates: [\n        [west, latitude],\n        [east, latitude],\n      ],\n    },\n  }\n}\n\nconst createLongitudeFeature = (\n  longitude: number,\n  south: number,\n  north: number\n): GeoJSON.Feature<GeoJSON.LineString> => {\n  const normalizedLongitude =\n    ((((longitude + LONGITUDE_WRAP) % LONGITUDE_CYCLE) + LONGITUDE_CYCLE) % LONGITUDE_CYCLE) - LONGITUDE_WRAP\n\n  return {\n    type: \"Feature\",\n    properties: { longitude: normalizedLongitude },\n    geometry: {\n      type: \"LineString\",\n      coordinates: [\n        [longitude, Math.max(south, MIN_RENDER_LATITUDE)],\n        [longitude, Math.min(north, MAX_RENDER_LATITUDE)],\n      ],\n    },\n  }\n}\n\nconst createLatitudeLabel = (latitude: number, west: number, map: mapboxgl.Map): GridLabel => {\n  const point = map.project([west, latitude])\n  const direction = latitude >= 0 ? \"N\" : \"S\"\n\n  return {\n    id: `lat-${latitude}`,\n    text: `${Math.abs(latitude)}°${direction}`,\n    position: { x: point.x + LABEL_OFFSET_X, y: point.y },\n    type: \"lat\",\n  }\n}\n\nconst createLongitudeLabel = (longitude: number, north: number, map: mapboxgl.Map): GridLabel => {\n  const normalizedLongitude =\n    ((((longitude + LONGITUDE_WRAP) % LONGITUDE_CYCLE) + LONGITUDE_CYCLE) % LONGITUDE_CYCLE) - LONGITUDE_WRAP\n  const point = map.project([longitude, north])\n  const direction = normalizedLongitude >= 0 ? \"E\" : \"W\"\n\n  return {\n    id: `lng-${longitude}`,\n    text: `${Math.abs(normalizedLongitude)}°${direction}`,\n    position: { x: point.x, y: point.y + LABEL_OFFSET_Y },\n    type: \"lng\",\n  }\n}\n\nconst generateGridLines = (\n  bounds: mapboxgl.LngLatBounds,\n  latitudeInterval: number,\n  longitudeInterval: number,\n  map: mapboxgl.Map\n): GridLinesResult => {\n  const latitudeLines: GeoJSON.Feature<GeoJSON.LineString>[] = []\n  const longitudeLines: GeoJSON.Feature<GeoJSON.LineString>[] = []\n  const labels: GridLabel[] = []\n\n  const south = bounds.getSouth()\n  const north = bounds.getNorth()\n  const west = bounds.getWest()\n  const east = bounds.getEast()\n\n  const startLatitude = Math.floor(south / latitudeInterval) * latitudeInterval\n  const endLatitude = Math.ceil(north / latitudeInterval) * latitudeInterval\n\n  for (let latitude = startLatitude; latitude <= endLatitude; latitude += latitudeInterval) {\n    if (latitude >= MIN_LATITUDE && latitude <= MAX_LATITUDE) {\n      latitudeLines.push(createLatitudeFeature(latitude, west, east))\n      labels.push(createLatitudeLabel(latitude, west, map))\n    }\n  }\n\n  const startLongitude = Math.floor(west / longitudeInterval) * longitudeInterval\n  const endLongitude = Math.ceil(east / longitudeInterval) * longitudeInterval\n\n  for (let longitude = startLongitude; longitude <= endLongitude; longitude += longitudeInterval) {\n    longitudeLines.push(createLongitudeFeature(longitude, south, north))\n    labels.push(createLongitudeLabel(longitude, north, map))\n  }\n\n  return { latitudeLines, longitudeLines, labels }\n}\n\nconst createLabelElement = (\n  label: GridLabel,\n  labelColor: string,\n  labelSize: number,\n  labelBackground: string\n): HTMLDivElement => {\n  const element = document.createElement(\"div\")\n  element.className = \"map-grid-label\"\n\n  const translateX = label.type === \"lng\" ? \"-50%\" : \"0\"\n  const translateY = label.type === \"lat\" ? \"-50%\" : \"0\"\n\n  element.style.cssText = `\n    position: absolute;\n    left: ${label.position.x}px;\n    top: ${label.position.y}px;\n    color: ${labelColor};\n    font-size: ${labelSize}px;\n    background: ${labelBackground};\n    padding: ${LABEL_PADDING};\n    border-radius: ${LABEL_BORDER_RADIUS};\n    pointer-events: none;\n    white-space: nowrap;\n    font-family: monospace;\n    transform: translate(${translateX}, ${translateY});\n  `\n  element.textContent = label.text\n\n  return element\n}\n\nconst updateSourceData = (map: mapboxgl.Map, sourceId: string, geoJson: GeoJSON.FeatureCollection) => {\n  const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource | undefined\n\n  if (source) {\n    source.setData(geoJson)\n  } else {\n    map.addSource(sourceId, { type: \"geojson\", data: geoJson })\n  }\n}\n\nconst addLineLayer = (\n  map: mapboxgl.Map,\n  layerId: string,\n  sourceId: string,\n  lineColor: string,\n  lineOpacity: number,\n  lineWidth: number\n) => {\n  if (map.getLayer(layerId)) {\n    return\n  }\n\n  map.addLayer({\n    id: layerId,\n    type: \"line\",\n    source: sourceId,\n    paint: {\n      \"line-color\": lineColor,\n      \"line-opacity\": lineOpacity,\n      \"line-width\": lineWidth,\n    },\n  })\n}\n\nconst cleanupLayers = (\n  map: mapboxgl.Map,\n  latLayerId: string,\n  lngLayerId: string,\n  latSourceId: string,\n  lngSourceId: string\n) => {\n  if (!map || !map.getStyle()) {\n    return\n  }\n\n  if (map.getLayer(latLayerId)) {\n    map.removeLayer(latLayerId)\n  }\n  if (map.getLayer(lngLayerId)) {\n    map.removeLayer(lngLayerId)\n  }\n  if (map.getSource(latSourceId)) {\n    map.removeSource(latSourceId)\n  }\n  if (map.getSource(lngSourceId)) {\n    map.removeSource(lngSourceId)\n  }\n}\n\nexport const MapGrid = ({\n  id = DEFAULT_ID,\n  latitudeInterval = DEFAULT_LATITUDE_INTERVAL,\n  longitudeInterval = DEFAULT_LONGITUDE_INTERVAL,\n  lineColor = DEFAULT_LINE_COLOR,\n  lineOpacity = DEFAULT_LINE_OPACITY,\n  lineWidth = DEFAULT_LINE_WIDTH,\n  showLabels = true,\n  labelColor = DEFAULT_LABEL_COLOR,\n  labelSize = DEFAULT_LABEL_SIZE,\n  labelBackground = DEFAULT_LABEL_BACKGROUND,\n}: MapGridProps) => {\n  const { map, isLoaded } = useMap()\n  const containerRef = useRef<HTMLDivElement | null>(null)\n\n  const lineColorRef = useRef(lineColor)\n  lineColorRef.current = lineColor\n  const lineOpacityRef = useRef(lineOpacity)\n  lineOpacityRef.current = lineOpacity\n  const lineWidthRef = useRef(lineWidth)\n  lineWidthRef.current = lineWidth\n  const showLabelsRef = useRef(showLabels)\n  showLabelsRef.current = showLabels\n  const labelColorRef = useRef(labelColor)\n  labelColorRef.current = labelColor\n  const labelSizeRef = useRef(labelSize)\n  labelSizeRef.current = labelSize\n  const labelBackgroundRef = useRef(labelBackground)\n  labelBackgroundRef.current = labelBackground\n\n  const latSourceId = `${id}-lat-source`\n  const lngSourceId = `${id}-lng-source`\n  const latLayerId = `${id}-lat-layer`\n  const lngLayerId = `${id}-lng-layer`\n\n  useEffect(() => {\n    if (!isLoaded || !map) {\n      return\n    }\n\n    if (latitudeInterval <= 0 || longitudeInterval <= 0) {\n      return\n    }\n\n    let frameId: number\n\n    const updateGrid = () => {\n      cancelAnimationFrame(frameId)\n\n      frameId = requestAnimationFrame(() => {\n        const bounds = map.getBounds()\n        if (!bounds) {\n          return\n        }\n\n        const { latitudeLines, longitudeLines, labels } = generateGridLines(\n          bounds,\n          latitudeInterval,\n          longitudeInterval,\n          map\n        )\n\n        const latGeoJson: GeoJSON.FeatureCollection = {\n          type: \"FeatureCollection\",\n          features: latitudeLines,\n        }\n\n        const lngGeoJson: GeoJSON.FeatureCollection = {\n          type: \"FeatureCollection\",\n          features: longitudeLines,\n        }\n\n        updateSourceData(map, latSourceId, latGeoJson)\n        updateSourceData(map, lngSourceId, lngGeoJson)\n        addLineLayer(map, latLayerId, latSourceId, lineColorRef.current, lineOpacityRef.current, lineWidthRef.current)\n        addLineLayer(map, lngLayerId, lngSourceId, lineColorRef.current, lineOpacityRef.current, lineWidthRef.current)\n\n        if (containerRef.current) {\n          containerRef.current.innerHTML = \"\"\n\n          if (showLabelsRef.current) {\n            for (const label of labels) {\n              const element = createLabelElement(\n                label,\n                labelColorRef.current,\n                labelSizeRef.current,\n                labelBackgroundRef.current\n              )\n              containerRef.current?.appendChild(element)\n            }\n          }\n        }\n      })\n    }\n\n    updateGrid()\n\n    map.on(\"move\", updateGrid)\n\n    return () => {\n      cancelAnimationFrame(frameId)\n      map.off(\"move\", updateGrid)\n      cleanupLayers(map, latLayerId, lngLayerId, latSourceId, lngSourceId)\n    }\n  }, [map, isLoaded, latitudeInterval, longitudeInterval, latSourceId, lngSourceId, latLayerId, lngLayerId])\n\n  useEffect(() => {\n    if (!isLoaded || !map) {\n      return\n    }\n\n    if (map.getLayer(latLayerId)) {\n      map.setPaintProperty(latLayerId, \"line-color\", lineColor)\n      map.setPaintProperty(latLayerId, \"line-opacity\", lineOpacity)\n      map.setPaintProperty(latLayerId, \"line-width\", lineWidth)\n    }\n    if (map.getLayer(lngLayerId)) {\n      map.setPaintProperty(lngLayerId, \"line-color\", lineColor)\n      map.setPaintProperty(lngLayerId, \"line-opacity\", lineOpacity)\n      map.setPaintProperty(lngLayerId, \"line-width\", lineWidth)\n    }\n  }, [map, isLoaded, lineColor, lineOpacity, lineWidth, latLayerId, lngLayerId])\n\n  if (!isLoaded || !map) {\n    return null\n  }\n\n  const mapContainer = map.getContainer()\n\n  return createPortal(\n    <div\n      ref={containerRef}\n      style={{\n        position: \"absolute\",\n        top: 0,\n        left: 0,\n        width: \"100%\",\n        height: \"100%\",\n        pointerEvents: \"none\",\n        overflow: \"hidden\",\n      }}\n    />,\n    mapContainer\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/grid.tsx"
    }
  ],
  "type": "registry:ui"
}
