{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sync",
  "title": "Map Sync",
  "description": "Synchronized multi-map component with bidirectional movement sync.",
  "dependencies": ["mapbox-gl", "next-themes", "lucide-react"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/map-sync.tsx",
      "content": "\"use client\"\n\nimport { createContext, useContext, useEffect, useRef, useState, type ReactNode } from \"react\"\nimport { useTheme } from \"next-themes\"\nimport type mapboxgl from \"mapbox-gl\"\nimport { mapgl, detectedLibrary } from \"./map-library\"\nimport { Globe } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport {\n  defaultMapStyles,\n  defaultMapLibreStyles,\n  type MapProjection,\n  type MapCoordinates,\n  type MapThemeStyles,\n  type MapSyncLayout,\n} from \"./types\"\n\ntype MapConfig = {\n  style?: string\n  styles?: MapThemeStyles\n  label?: string\n}\n\ntype MapSyncProps = {\n  accessToken?: string\n  maps: [MapConfig, MapConfig] | [MapConfig, MapConfig, MapConfig, MapConfig]\n  center?: MapCoordinates\n  zoom?: number\n  bearing?: number\n  pitch?: number\n  projection?: MapProjection\n  layout?: MapSyncLayout\n  showLabels?: boolean\n  loader?: ReactNode\n}\n\ntype MapSyncContextValue = {\n  registerMap: (index: number, map: mapboxgl.Map) => void\n  unregisterMap: (index: number) => void\n  accessToken?: string\n  initialCenter: MapCoordinates\n  initialZoom: number\n  initialBearing: number\n  initialPitch: number\n  projection?: MapProjection\n}\n\ntype MapSyncProviderProps = {\n  children: ReactNode\n  accessToken?: string\n  initialCenter: MapCoordinates\n  initialZoom: number\n  initialBearing: number\n  initialPitch: number\n  projection?: MapProjection\n}\n\ntype SyncedMapPanelProps = {\n  index: number\n  style: { width: string; height: string }\n  label: string\n  showLabel: boolean\n  mapStyle?: string\n  mapStyles?: MapThemeStyles\n  loader?: ReactNode\n}\n\nconst DEFAULT_CENTER: MapCoordinates = [0, 0]\nconst DEFAULT_ZOOM = 2\nconst DEFAULT_BEARING = 0\nconst DEFAULT_PITCH = 0\n\nconst LABEL_BASE_STYLES = \"absolute z-10 bg-background/95 backdrop-blur-sm border rounded px-2 py-1 text-xs font-medium\"\n\nconst MapSyncContext = createContext<MapSyncContextValue | null>(null)\n\nconst useMapSync = () => {\n  const context = useContext(MapSyncContext)\n  if (!context) {\n    throw new Error(\"useMapSync must be used within MapSyncProvider\")\n  }\n  return context\n}\n\nexport const MapSync = ({\n  accessToken,\n  maps,\n  center = DEFAULT_CENTER,\n  zoom = DEFAULT_ZOOM,\n  bearing = DEFAULT_BEARING,\n  pitch = DEFAULT_PITCH,\n  projection,\n  layout = \"horizontal\",\n  showLabels = false,\n  loader,\n}: MapSyncProps) => {\n  const mapCount = maps.length\n  const isGrid = layout === \"grid\" && mapCount === 4\n\n  const getLayoutClasses = () => {\n    if (isGrid) {\n      return \"grid grid-cols-2 grid-rows-2\"\n    }\n    if (layout === \"vertical\") {\n      return \"flex flex-col\"\n    }\n    return \"flex flex-row\"\n  }\n\n  const getPanelSize = () => {\n    if (isGrid) {\n      return { width: \"100%\", height: \"100%\" }\n    }\n    if (layout === \"vertical\") {\n      return { width: \"100%\", height: `${100 / mapCount}%` }\n    }\n    return { width: `${100 / mapCount}%`, height: \"100%\" }\n  }\n\n  const getDefaultLabel = (index: number) => {\n    if (mapCount === 2) {\n      if (layout === \"vertical\") {\n        return index === 0 ? \"Top\" : \"Bottom\"\n      }\n      return index === 0 ? \"Left\" : \"Right\"\n    }\n    const labels = [\"Top Left\", \"Top Right\", \"Bottom Left\", \"Bottom Right\"]\n    return labels[index]\n  }\n\n  return (\n    <MapSyncProvider\n      accessToken={accessToken}\n      initialCenter={center}\n      initialZoom={zoom}\n      initialBearing={bearing}\n      initialPitch={pitch}\n      projection={projection}\n    >\n      <div className={cn(\"relative w-full h-full\", getLayoutClasses())}>\n        {maps.map((config, index) => {\n          return (\n            <SyncedMapPanel\n              key={index}\n              index={index}\n              style={getPanelSize()}\n              label={config.label ?? getDefaultLabel(index)}\n              showLabel={showLabels}\n              mapStyle={config.style}\n              mapStyles={config.styles}\n              loader={loader}\n            />\n          )\n        })}\n      </div>\n    </MapSyncProvider>\n  )\n}\n\nconst MapSyncProvider = ({\n  children,\n  accessToken,\n  initialCenter,\n  initialZoom,\n  initialBearing,\n  initialPitch,\n  projection,\n}: MapSyncProviderProps) => {\n  const mapsRef = useRef<Map<number, mapboxgl.Map>>(new Map())\n  const isSyncingRef = useRef(false)\n\n  const syncMaps = (sourceIndex: number, sourceMap: mapboxgl.Map) => {\n    if (isSyncingRef.current) {\n      return\n    }\n\n    isSyncingRef.current = true\n\n    const center = sourceMap.getCenter()\n    const zoom = sourceMap.getZoom()\n    const bearing = sourceMap.getBearing()\n    const pitch = sourceMap.getPitch()\n\n    mapsRef.current.forEach((map, index) => {\n      if (index === sourceIndex) {\n        return\n      }\n      map.jumpTo({\n        center,\n        zoom,\n        bearing,\n        pitch,\n      })\n    })\n\n    isSyncingRef.current = false\n  }\n\n  const registerMap = (index: number, map: mapboxgl.Map) => {\n    mapsRef.current.set(index, map)\n\n    const handleMove = () => {\n      syncMaps(index, map)\n    }\n\n    map.on(\"move\", handleMove)\n  }\n\n  const unregisterMap = (index: number) => {\n    mapsRef.current.delete(index)\n  }\n\n  const contextValue: MapSyncContextValue = {\n    registerMap,\n    unregisterMap,\n    accessToken,\n    initialCenter,\n    initialZoom,\n    initialBearing,\n    initialPitch,\n    projection,\n  }\n\n  return <MapSyncContext.Provider value={contextValue}>{children}</MapSyncContext.Provider>\n}\n\nconst SyncedMapPanel = ({ index, style, label, showLabel, mapStyle, mapStyles, loader }: SyncedMapPanelProps) => {\n  const containerRef = useRef<HTMLDivElement>(null)\n  const mapRef = useRef<mapboxgl.Map | null>(null)\n  const [isLoaded, setIsLoaded] = useState(false)\n  const initializedRef = useRef(false)\n  const { resolvedTheme } = useTheme()\n  const {\n    registerMap,\n    unregisterMap,\n    accessToken,\n    initialCenter,\n    initialZoom,\n    initialBearing,\n    initialPitch,\n    projection,\n  } = useMapSync()\n\n  const getMapStyle = () => {\n    if (mapStyle) {\n      return mapStyle\n    }\n    const defaults = detectedLibrary === \"maplibre\" ? defaultMapLibreStyles : defaultMapStyles\n    const darkStyle = mapStyles?.dark ?? defaults.dark\n    const lightStyle = mapStyles?.light ?? defaults.light\n    return resolvedTheme === \"dark\" ? darkStyle : lightStyle\n  }\n\n  useEffect(() => {\n    if (initializedRef.current) {\n      return\n    }\n    if (!containerRef.current) {\n      return\n    }\n\n    initializedRef.current = true\n\n    if (accessToken) {\n      mapgl.accessToken = accessToken\n    }\n\n    const map = new mapgl.Map({\n      container: containerRef.current,\n      style: getMapStyle(),\n      center: initialCenter,\n      zoom: initialZoom,\n      bearing: initialBearing,\n      pitch: initialPitch,\n      projection,\n      attributionControl: false,\n    })\n\n    map.on(\"load\", () => {\n      setIsLoaded(true)\n      registerMap(index, map)\n    })\n\n    mapRef.current = map\n\n    return () => {\n      unregisterMap(index)\n      map.remove()\n      mapRef.current = null\n      initializedRef.current = false\n    }\n  }, [])\n\n  useEffect(() => {\n    if (!mapRef.current || !isLoaded) {\n      return\n    }\n    mapRef.current.setStyle(getMapStyle())\n  }, [mapStyle, mapStyles, resolvedTheme])\n\n  return (\n    <div className=\"relative\" style={style}>\n      {showLabel && <div className={cn(LABEL_BASE_STYLES, \"top-3 left-3\")}>{label}</div>}\n      <div ref={containerRef} className=\"w-full h-full\">\n        {!isLoaded && (loader || <DefaultLoader />)}\n      </div>\n    </div>\n  )\n}\n\nconst DefaultLoader = () => {\n  return (\n    <div className=\"absolute inset-0 z-10 flex items-center justify-center bg-muted\">\n      <Globe className=\"size-8 text-muted-foreground/60 animate-spin\" />\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/map-sync.tsx"
    }
  ],
  "type": "registry:ui"
}
