{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "map",
  "title": "Map",
  "description": "Core SVG map renderer with region interaction, tooltips, and markers. Required by all map components.",
  "files": [
    {
      "path": "registry/shadcnmaps/map.tsx",
      "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport {\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\n\nimport { MapProvider, useMapContext } from './map-context'\nimport { MapListbox } from './map-listbox'\nimport { MapMarker } from './map-marker'\nimport { MapRegion } from './map-region'\nimport { MapTooltip } from './map-tooltip'\nimport type {\n  MapData,\n  MapMarkerData,\n  MapRegionData,\n  MarkerEvent,\n  RegionEvent,\n  RegionOverride,\n  ZoomConfig,\n} from './types'\n\nexport interface MapProps {\n  data: MapData\n  selectedRegion?: string | null\n  regions?: RegionOverride[]\n  markers?: MapMarkerData[]\n  disabledRegions?: string[]\n  onRegionClick?: (event: RegionEvent) => void\n  onRegionEnter?: (event: RegionEvent) => void\n  onRegionLeave?: (event: RegionEvent) => void\n  onMarkerClick?: (event: MarkerEvent) => void\n  showLabels?: boolean\n  showTooltips?: boolean\n  renderTooltip?: (region: MapRegionData) => ReactNode\n  className?: string\n  children?: ReactNode\n  'aria-label'?: string\n  enableZoom?: boolean\n  zoomConfig?: Partial<ZoomConfig>\n  controls?: ReactNode\n}\n\nfunction getPointerPosition(\n  nativeEvent: unknown\n): { x: number; y: number } | null {\n  if (\n    typeof nativeEvent === 'object' &&\n    nativeEvent !== null &&\n    'clientX' in nativeEvent &&\n    'clientY' in nativeEvent &&\n    typeof nativeEvent.clientX === 'number' &&\n    typeof nativeEvent.clientY === 'number'\n  ) {\n    return { x: nativeEvent.clientX, y: nativeEvent.clientY }\n  }\n\n  const maybeTouches = (\n    nativeEvent as {\n      touches?: {\n        length: number\n        item?: (index: number) => unknown\n        [index: number]: unknown\n      }\n    }\n  ).touches\n\n  if (maybeTouches && maybeTouches.length > 0) {\n    const touch =\n      typeof maybeTouches.item === 'function'\n        ? maybeTouches.item(0)\n        : maybeTouches[0]\n    if (\n      typeof touch === 'object' &&\n      touch !== null &&\n      'clientX' in touch &&\n      'clientY' in touch &&\n      typeof touch.clientX === 'number' &&\n      typeof touch.clientY === 'number'\n    ) {\n      return { x: touch.clientX, y: touch.clientY }\n    }\n  }\n\n  return null\n}\n\nfunction parseViewBox(viewBox: string) {\n  const parts = viewBox.split(/\\s+/).map(Number)\n  return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] }\n}\n\nfunction MapInner({\n  data,\n  regions,\n  markers,\n  disabledRegions,\n  onRegionClick,\n  onRegionEnter,\n  onRegionLeave,\n  onMarkerClick,\n  showLabels = true,\n  showTooltips = true,\n  renderTooltip,\n  className,\n  children,\n  'aria-label': ariaLabel,\n  enableZoom = false,\n}: MapProps) {\n  const {\n    selectedRegion,\n    setSelectedRegion,\n    focusedRegion,\n    setTooltipState,\n    zoomState,\n    setZoomState,\n    zoomConfig,\n  } = useMapContext()\n  const descId = useId()\n  const [announcement, setAnnouncement] = useState('')\n  const svgRef = useRef<SVGSVGElement>(null)\n  const isPanningRef = useRef(false)\n  const [isGesture, setIsGesture] = useState(false)\n  const dragStartRef = useRef<{\n    x: number\n    y: number\n    tx: number\n    ty: number\n  } | null>(null)\n  const pinchStartRef = useRef<{ dist: number; scale: number } | null>(null)\n\n  const regionOverrides = useMemo(() => {\n    return new Map(regions?.map((region) => [region.id, region]) ?? [])\n  }, [regions])\n\n  const disabledRegionSet = useMemo(() => {\n    return new Set(disabledRegions ?? [])\n  }, [disabledRegions])\n\n  const mergedRegions = useMemo(() => {\n    return data.regions.map((baseRegion) => {\n      const override = regionOverrides.get(baseRegion.id)\n      return {\n        ...baseRegion,\n        ...override,\n      }\n    })\n  }, [data.regions, regionOverrides])\n\n  const mapLabel = ariaLabel ?? data.name\n\n  const viewBox = useMemo(() => parseViewBox(data.viewBox), [data.viewBox])\n\n  const clampTranslate = useCallback(\n    (tx: number, ty: number, scale: number) => {\n      const maxPanX = (viewBox.width * (scale - 1)) / 2 + viewBox.width * 0.5\n      const maxPanY = (viewBox.height * (scale - 1)) / 2 + viewBox.height * 0.5\n      return {\n        tx: Math.max(-maxPanX, Math.min(maxPanX, tx)),\n        ty: Math.max(-maxPanY, Math.min(maxPanY, ty)),\n      }\n    },\n    [viewBox]\n  )\n\n  // Wheel zoom\n  useEffect(() => {\n    if (!enableZoom) return\n    const svg = svgRef.current\n    if (!svg) return\n\n    const handleWheel = (e: WheelEvent) => {\n      e.preventDefault()\n      const delta = e.deltaY > 0 ? -zoomConfig.zoomStep : zoomConfig.zoomStep\n\n      setZoomState((prev) => {\n        const newScale = Math.max(\n          zoomConfig.minZoom,\n          Math.min(zoomConfig.maxZoom, prev.scale + delta)\n        )\n        if (newScale === prev.scale) return prev\n\n        // Auto-center when fully zoomed out\n        if (newScale === zoomConfig.minZoom) {\n          return { scale: newScale, translateX: 0, translateY: 0 }\n        }\n\n        // Zoom toward cursor position\n        const rect = svg.getBoundingClientRect()\n        const cursorX = e.clientX - rect.left\n        const cursorY = e.clientY - rect.top\n        const svgX = (cursorX / rect.width) * viewBox.width + viewBox.x\n        const svgY = (cursorY / rect.height) * viewBox.height + viewBox.y\n\n        const scaleRatio = newScale / prev.scale\n        const newTx = svgX - scaleRatio * (svgX - prev.translateX)\n        const newTy = svgY - scaleRatio * (svgY - prev.translateY)\n        const clamped = clampTranslate(newTx, newTy, newScale)\n\n        return {\n          scale: newScale,\n          translateX: clamped.tx,\n          translateY: clamped.ty,\n        }\n      })\n    }\n\n    svg.addEventListener('wheel', handleWheel, { passive: false })\n    return () => svg.removeEventListener('wheel', handleWheel)\n  }, [enableZoom, zoomConfig, setZoomState, viewBox, clampTranslate])\n\n  // Click-and-drag pan — use document-level move/up so we don't steal\n  // pointer events from child <path> elements (setPointerCapture would\n  // prevent region clicks from firing).\n  const handlePointerDown = useCallback(\n    (e: ReactPointerEvent<SVGSVGElement>) => {\n      if (!enableZoom || e.button !== 0) return\n      // Only start drag from the SVG background or zoom layer, not from regions\n      const target = e.target as Element\n      if (target.closest('[data-slot=\"map-region\"]')) {\n        // Store start position so we can detect drag even from a region\n        isPanningRef.current = false\n        setIsGesture(true)\n        dragStartRef.current = {\n          x: e.clientX,\n          y: e.clientY,\n          tx: zoomState.translateX,\n          ty: zoomState.translateY,\n        }\n        return\n      }\n      isPanningRef.current = false\n      setIsGesture(true)\n      dragStartRef.current = {\n        x: e.clientX,\n        y: e.clientY,\n        tx: zoomState.translateX,\n        ty: zoomState.translateY,\n      }\n    },\n    [enableZoom, zoomState.translateX, zoomState.translateY]\n  )\n\n  // Document-level pointermove/pointerup for drag panning\n  useEffect(() => {\n    if (!enableZoom) return\n\n    const handlePointerMove = (e: PointerEvent) => {\n      if (!dragStartRef.current) return\n      const dx = e.clientX - dragStartRef.current.x\n      const dy = e.clientY - dragStartRef.current.y\n\n      // Movement threshold to distinguish from click\n      if (!isPanningRef.current && Math.hypot(dx, dy) < 3) return\n      isPanningRef.current = true\n\n      const svg = svgRef.current\n      if (!svg) return\n      const rect = svg.getBoundingClientRect()\n      const svgDx = (dx / rect.width) * viewBox.width * zoomState.scale\n      const svgDy = (dy / rect.height) * viewBox.height * zoomState.scale\n\n      const newTx = dragStartRef.current.tx + svgDx\n      const newTy = dragStartRef.current.ty + svgDy\n      const clamped = clampTranslate(newTx, newTy, zoomState.scale)\n\n      setZoomState((prev) => ({\n        ...prev,\n        translateX: clamped.tx,\n        translateY: clamped.ty,\n      }))\n    }\n\n    const handlePointerUp = () => {\n      if (!dragStartRef.current) return\n      dragStartRef.current = null\n      // Delay clearing so click handlers can check isPanning\n      requestAnimationFrame(() => {\n        isPanningRef.current = false\n        setIsGesture(false)\n      })\n    }\n\n    document.addEventListener('pointermove', handlePointerMove)\n    document.addEventListener('pointerup', handlePointerUp)\n    return () => {\n      document.removeEventListener('pointermove', handlePointerMove)\n      document.removeEventListener('pointerup', handlePointerUp)\n    }\n  }, [enableZoom, viewBox, zoomState.scale, setZoomState, clampTranslate])\n\n  // Touch pinch-to-zoom\n  useEffect(() => {\n    if (!enableZoom) return\n    const svg = svgRef.current\n    if (!svg) return\n\n    const getTouchDist = (e: TouchEvent) => {\n      if (e.touches.length < 2) return null\n      const dx = e.touches[0].clientX - e.touches[1].clientX\n      const dy = e.touches[0].clientY - e.touches[1].clientY\n      return Math.hypot(dx, dy)\n    }\n\n    const handleTouchStart = (e: TouchEvent) => {\n      if (e.touches.length === 2) {\n        e.preventDefault()\n        const dist = getTouchDist(e)\n        if (dist !== null) {\n          pinchStartRef.current = { dist, scale: zoomState.scale }\n        }\n      }\n    }\n\n    const handleTouchMove = (e: TouchEvent) => {\n      if (e.touches.length === 2 && pinchStartRef.current) {\n        e.preventDefault()\n        const dist = getTouchDist(e)\n        if (dist !== null) {\n          const scaleRatio = dist / pinchStartRef.current.dist\n          const newScale = Math.max(\n            zoomConfig.minZoom,\n            Math.min(\n              zoomConfig.maxZoom,\n              pinchStartRef.current.scale * scaleRatio\n            )\n          )\n          setZoomState((prev) => ({ ...prev, scale: newScale }))\n        }\n      }\n    }\n\n    const handleTouchEnd = (e: TouchEvent) => {\n      if (e.touches.length < 2) {\n        pinchStartRef.current = null\n      }\n    }\n\n    svg.addEventListener('touchstart', handleTouchStart, { passive: false })\n    svg.addEventListener('touchmove', handleTouchMove, { passive: false })\n    svg.addEventListener('touchend', handleTouchEnd)\n    return () => {\n      svg.removeEventListener('touchstart', handleTouchStart)\n      svg.removeEventListener('touchmove', handleTouchMove)\n      svg.removeEventListener('touchend', handleTouchEnd)\n    }\n  }, [enableZoom, zoomConfig, zoomState.scale, setZoomState])\n\n  const shouldSuppressClick = useCallback(() => {\n    return enableZoom && isPanningRef.current\n  }, [enableZoom])\n\n  const svgContent = (\n    <>\n      {mergedRegions.map((region) => {\n        const disabled = region.disabled || disabledRegionSet.has(region.id)\n        const tooltipContent =\n          renderTooltip?.(region) ?? region.tooltipContent ?? region.name\n\n        return (\n          <MapRegion\n            key={region.id}\n            id={region.id}\n            name={region.name}\n            path={region.path}\n            abbreviation={showLabels ? region.abbreviation : undefined}\n            labelX={showLabels ? region.labelX : undefined}\n            labelY={showLabels ? region.labelY : undefined}\n            metadata={region.metadata}\n            className={region.className}\n            labelClassName={region.labelClassName}\n            disabled={disabled}\n            onClick={(event) => {\n              if (shouldSuppressClick()) return\n              const isCurrentlySelected = selectedRegion === region.id\n              setSelectedRegion(isCurrentlySelected ? null : region.id)\n              setAnnouncement(\n                isCurrentlySelected\n                  ? `${region.name} deselected`\n                  : `${region.name} selected`\n              )\n              onRegionClick?.(event)\n\n              if (showTooltips) {\n                setTooltipState({\n                  visible: true,\n                  content: tooltipContent,\n                  position: getPointerPosition(event.nativeEvent),\n                })\n              }\n            }}\n            onEnter={(event) => {\n              onRegionEnter?.(event)\n              if (showTooltips) {\n                setTooltipState((current) => ({\n                  visible: true,\n                  content: tooltipContent,\n                  position:\n                    getPointerPosition(event.nativeEvent) ?? current.position,\n                }))\n              }\n            }}\n            onMove={(event) => {\n              if (!showTooltips) {\n                return\n              }\n\n              setTooltipState((current) => ({\n                ...current,\n                visible: true,\n                content: tooltipContent,\n                position:\n                  getPointerPosition(event.nativeEvent) ?? current.position,\n              }))\n            }}\n            onLeave={(event) => {\n              onRegionLeave?.(event)\n\n              if (showTooltips) {\n                setTooltipState((current) => ({ ...current, visible: false }))\n              }\n            }}\n          />\n        )\n      })}\n      {markers?.map((marker) => (\n        <MapMarker\n          key={marker.id}\n          id={marker.id}\n          x={marker.x}\n          y={marker.y}\n          content={marker.content}\n          label={marker.label}\n          disabled={marker.disabled}\n          onClick={(event) => {\n            if (shouldSuppressClick()) return\n            onMarkerClick?.(event)\n\n            if (showTooltips && marker.tooltipContent) {\n              setTooltipState({\n                visible: true,\n                content: marker.tooltipContent,\n                position: getPointerPosition(event.nativeEvent),\n              })\n            }\n          }}\n          onEnter={(event) => {\n            if (showTooltips && marker.tooltipContent) {\n              setTooltipState({\n                visible: true,\n                content: marker.tooltipContent,\n                position: getPointerPosition(event.nativeEvent),\n              })\n            }\n          }}\n          onMove={(event) => {\n            if (!showTooltips || !marker.tooltipContent) {\n              return\n            }\n\n            setTooltipState((current) => ({\n              ...current,\n              visible: true,\n              content: marker.tooltipContent,\n              position:\n                getPointerPosition(event.nativeEvent) ?? current.position,\n            }))\n          }}\n          onLeave={() => {\n            if (showTooltips) {\n              setTooltipState((current) => ({ ...current, visible: false }))\n            }\n          }}\n        />\n      ))}\n      {children}\n      <FocusRingOverlay focusedRegion={focusedRegion} regions={mergedRegions} />\n    </>\n  )\n\n  return (\n    <>\n      <svg\n        ref={svgRef}\n        data-slot='map'\n        role='group'\n        aria-label={mapLabel}\n        aria-describedby={descId}\n        viewBox={data.viewBox}\n        className={cn('h-auto w-full', className)}\n        style={\n          enableZoom\n            ? {\n                touchAction: 'none',\n                cursor: zoomState.scale > 1 ? 'grab' : undefined,\n              }\n            : undefined\n        }\n        onPointerDown={enableZoom ? handlePointerDown : undefined}\n        onPointerLeave={() => {\n          if (showTooltips) {\n            setTooltipState((current) => ({ ...current, visible: false }))\n          }\n        }}\n      >\n        <desc id={descId}>\n          Interactive map. Tab to focus, arrow keys to navigate between regions,\n          Enter or Space to select, Escape to clear.\n        </desc>\n        {enableZoom ? (\n          <g\n            data-slot='map-zoom-layer'\n            transform={`translate(${zoomState.translateX},${zoomState.translateY}) scale(${zoomState.scale})`}\n            style={{\n              transition: isGesture ? 'none' : 'transform 200ms ease-out',\n              willChange: 'transform',\n            }}\n          >\n            {svgContent}\n          </g>\n        ) : (\n          svgContent\n        )}\n      </svg>\n      <MapListbox\n        regions={mergedRegions}\n        disabledRegions={disabledRegionSet}\n        label={mapLabel}\n        onSelect={(region) => {\n          const isCurrentlySelected = selectedRegion === region.id\n          setAnnouncement(\n            isCurrentlySelected\n              ? `${region.name} deselected`\n              : `${region.name} selected`\n          )\n          onRegionClick?.({ region, nativeEvent: null })\n        }}\n      />\n      <div\n        role='status'\n        aria-live='polite'\n        aria-atomic='true'\n        className='sr-only'\n      >\n        {announcement}\n      </div>\n      {showTooltips ? <MapTooltipContainer /> : null}\n    </>\n  )\n}\n\nfunction FocusRingOverlay({\n  focusedRegion,\n  regions,\n}: {\n  focusedRegion: string | null\n  regions: MapRegionData[]\n}) {\n  if (!focusedRegion) return null\n  const region = regions.find((r) => r.id === focusedRegion)\n  if (!region) return null\n\n  return (\n    <path\n      data-slot='map-focus-ring'\n      d={region.path}\n      aria-hidden='true'\n      className='pointer-events-none fill-none stroke-map-region-focus-ring stroke-2'\n    />\n  )\n}\n\nfunction MapTooltipContainer() {\n  const { tooltipState } = useMapContext()\n\n  if (!tooltipState.content) {\n    return null\n  }\n\n  return (\n    <MapTooltip\n      visible={tooltipState.visible}\n      content={tooltipState.content}\n      position={tooltipState.position}\n      className='shadcnmaps-tooltip'\n    />\n  )\n}\n\nfunction MapRoot({\n  enableZoom,\n  zoomConfig,\n  controls,\n  selectedRegion: selectedRegionProp,\n  ...props\n}: MapProps) {\n  const viewBox = useMemo(\n    () => parseViewBox(props.data.viewBox),\n    [props.data.viewBox]\n  )\n  return (\n    <MapProvider\n      zoomConfig={zoomConfig}\n      viewBox={viewBox}\n      selectedRegion={selectedRegionProp}\n    >\n      <div data-slot='map-container' className='relative'>\n        <MapInner enableZoom={enableZoom} {...props} />\n        {controls}\n      </div>\n    </MapProvider>\n  )\n}\n\nexport { MapRoot as Map }\n",
      "type": "registry:file",
      "target": "components/shadcnmaps/map.tsx"
    },
    {
      "path": "registry/shadcnmaps/map-context.tsx",
      "content": "'use client'\n\nimport {\n  createContext,\n  type Dispatch,\n  type ReactNode,\n  type RefObject,\n  type SetStateAction,\n  useCallback,\n  useContext,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\n\nimport type { ZoomConfig, ZoomState } from './types'\n\nexport interface TooltipState {\n  visible: boolean\n  content: ReactNode | null\n  position: { x: number; y: number } | null\n}\n\nconst DEFAULT_ZOOM_STATE: ZoomState = {\n  scale: 1,\n  translateX: 0,\n  translateY: 0,\n}\n\nconst DEFAULT_ZOOM_CONFIG: ZoomConfig = {\n  minZoom: 1,\n  maxZoom: 8,\n  zoomStep: 0.5,\n  panStep: 50,\n}\n\ninterface MapContextValue {\n  selectedRegion: string | null\n  setSelectedRegion: Dispatch<SetStateAction<string | null>>\n  hoveredRegion: string | null\n  setHoveredRegion: Dispatch<SetStateAction<string | null>>\n  focusedRegion: string | null\n  setFocusedRegion: Dispatch<SetStateAction<string | null>>\n  tooltipState: TooltipState\n  setTooltipState: Dispatch<SetStateAction<TooltipState>>\n  zoomState: ZoomState\n  setZoomState: Dispatch<SetStateAction<ZoomState>>\n  zoomConfig: ZoomConfig\n  zoomIn: () => void\n  zoomOut: () => void\n  resetZoom: () => void\n  containerRef: RefObject<HTMLDivElement | null>\n}\n\nconst MapContext = createContext<MapContextValue | null>(null)\n\ninterface MapProviderProps {\n  children: ReactNode\n  selectedRegion?: string | null\n  zoomConfig?: Partial<ZoomConfig>\n  viewBox?: { x: number; y: number; width: number; height: number }\n}\n\nfunction zoomTowardCenter(\n  prev: ZoomState,\n  newScale: number,\n  viewBox: { x: number; y: number; width: number; height: number }\n): ZoomState {\n  const cx = viewBox.x + viewBox.width / 2\n  const cy = viewBox.y + viewBox.height / 2\n  const scaleRatio = newScale / prev.scale\n  return {\n    scale: newScale,\n    translateX: cx - scaleRatio * (cx - prev.translateX),\n    translateY: cy - scaleRatio * (cy - prev.translateY),\n  }\n}\n\nexport function MapProvider({\n  children,\n  selectedRegion: selectedRegionProp,\n  zoomConfig: zoomConfigProp,\n  viewBox,\n}: MapProviderProps) {\n  const isControlled = selectedRegionProp !== undefined\n  const [uncontrolledRegion, setUncontrolledRegion] = useState<string | null>(\n    null\n  )\n  const selectedRegion = isControlled ? selectedRegionProp : uncontrolledRegion\n  const noop = useCallback<\n    Dispatch<SetStateAction<string | null>>\n  >(() => {}, [])\n  const setSelectedRegion = isControlled ? noop : setUncontrolledRegion\n  const [hoveredRegion, setHoveredRegion] = useState<string | null>(null)\n  const [focusedRegion, setFocusedRegion] = useState<string | null>(null)\n  const [tooltipState, setTooltipState] = useState<TooltipState>({\n    visible: false,\n    content: null,\n    position: null,\n  })\n  const [zoomState, setZoomState] = useState<ZoomState>(DEFAULT_ZOOM_STATE)\n  const containerRef = useRef<HTMLDivElement>(null)\n\n  const zoomConfig = useMemo<ZoomConfig>(\n    () => ({ ...DEFAULT_ZOOM_CONFIG, ...zoomConfigProp }),\n    [zoomConfigProp]\n  )\n\n  const zoomIn = useCallback(() => {\n    setZoomState((prev) => {\n      const newScale = Math.min(\n        prev.scale + zoomConfig.zoomStep,\n        zoomConfig.maxZoom\n      )\n      if (!viewBox || newScale === prev.scale)\n        return { ...prev, scale: newScale }\n      return zoomTowardCenter(prev, newScale, viewBox)\n    })\n  }, [zoomConfig, viewBox])\n\n  const zoomOut = useCallback(() => {\n    setZoomState((prev) => {\n      const newScale = Math.max(\n        prev.scale - zoomConfig.zoomStep,\n        zoomConfig.minZoom\n      )\n      if (newScale === prev.scale) return prev\n      // Auto-center when fully zoomed out\n      if (newScale === zoomConfig.minZoom) {\n        return { scale: newScale, translateX: 0, translateY: 0 }\n      }\n      if (!viewBox) return { ...prev, scale: newScale }\n      return zoomTowardCenter(prev, newScale, viewBox)\n    })\n  }, [zoomConfig, viewBox])\n\n  const resetZoom = useCallback(() => {\n    setZoomState(DEFAULT_ZOOM_STATE)\n  }, [])\n\n  const value = useMemo<MapContextValue>(\n    () => ({\n      selectedRegion,\n      setSelectedRegion,\n      hoveredRegion,\n      setHoveredRegion,\n      focusedRegion,\n      setFocusedRegion,\n      tooltipState,\n      setTooltipState,\n      zoomState,\n      setZoomState,\n      zoomConfig,\n      zoomIn,\n      zoomOut,\n      resetZoom,\n      containerRef,\n    }),\n    [\n      focusedRegion,\n      hoveredRegion,\n      selectedRegion,\n      setSelectedRegion,\n      tooltipState,\n      zoomState,\n      zoomConfig,\n      zoomIn,\n      zoomOut,\n      resetZoom,\n    ]\n  )\n\n  return <MapContext.Provider value={value}>{children}</MapContext.Provider>\n}\n\nexport function useMapContext() {\n  const context = useContext(MapContext)\n  if (!context) {\n    throw new Error('useMapContext must be used within a MapProvider.')\n  }\n\n  return context\n}\n",
      "type": "registry:file",
      "target": "components/shadcnmaps/map-context.tsx"
    },
    {
      "path": "registry/shadcnmaps/map-region.tsx",
      "content": "'use client'\n\nimport { cn } from '@/lib/utils'\n\nimport { useMapContext } from './map-context'\nimport type { MapRegionData, RegionEvent } from './types'\n\nexport interface MapRegionProps extends MapRegionData {\n  selected?: boolean\n  hovered?: boolean\n  disabled?: boolean\n  className?: string\n  labelClassName?: string\n  onClick?: (event: RegionEvent) => void\n  onEnter?: (event: RegionEvent) => void\n  onLeave?: (event: RegionEvent) => void\n  onMove?: (event: RegionEvent) => void\n}\n\nexport function MapRegion({\n  id,\n  path,\n  name,\n  abbreviation,\n  labelX,\n  labelY,\n  metadata,\n  selected,\n  hovered,\n  disabled,\n  className,\n  labelClassName,\n  onClick,\n  onEnter,\n  onLeave,\n  onMove,\n}: MapRegionProps) {\n  const { hoveredRegion, selectedRegion, setHoveredRegion } = useMapContext()\n\n  const isSelected = selected ?? selectedRegion === id\n  const isHovered = hovered ?? hoveredRegion === id\n\n  const regionData: MapRegionData = {\n    id,\n    path,\n    name,\n    abbreviation,\n    labelX,\n    labelY,\n    metadata,\n  }\n\n  return (\n    <>\n      <path\n        data-slot='map-region'\n        d={path}\n        role='button'\n        tabIndex={-1}\n        aria-label={name ?? id}\n        aria-pressed={isSelected}\n        aria-disabled={disabled || undefined}\n        className={cn(\n          'cursor-pointer fill-map-region stroke-map-region-stroke stroke-1 outline-none motion-safe:transition-colors motion-safe:duration-150',\n          isHovered && 'fill-map-region-hover stroke-map-region-stroke-hover',\n          isSelected && 'fill-map-region-selected',\n          disabled && 'cursor-default fill-map-region-disabled opacity-60',\n          className\n        )}\n        onClick={(nativeEvent) => {\n          if (disabled) {\n            return\n          }\n\n          onClick?.({ region: regionData, nativeEvent })\n        }}\n        onMouseEnter={(nativeEvent) => {\n          if (disabled) {\n            return\n          }\n\n          setHoveredRegion(id)\n          onEnter?.({ region: regionData, nativeEvent })\n        }}\n        onMouseLeave={(nativeEvent) => {\n          if (disabled) {\n            return\n          }\n\n          setHoveredRegion((current) => (current === id ? null : current))\n          onLeave?.({ region: regionData, nativeEvent })\n        }}\n        onMouseMove={(nativeEvent) => {\n          if (disabled) {\n            return\n          }\n\n          onMove?.({ region: regionData, nativeEvent })\n        }}\n      />\n      {abbreviation && labelX !== undefined && labelY !== undefined ? (\n        <text\n          data-slot='map-region-label'\n          x={labelX}\n          y={labelY}\n          aria-hidden='true'\n          className={cn(\n            'pointer-events-none fill-map-label text-[10px] font-medium select-none motion-safe:transition-colors motion-safe:duration-150',\n            isHovered && 'fill-map-label-hover',\n            isSelected && 'fill-map-label-selected',\n            labelClassName\n          )}\n          textAnchor='middle'\n          dominantBaseline='middle'\n        >\n          {abbreviation}\n        </text>\n      ) : null}\n    </>\n  )\n}\n",
      "type": "registry:file",
      "target": "components/shadcnmaps/map-region.tsx"
    },
    {
      "path": "registry/shadcnmaps/map-tooltip.tsx",
      "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { type ReactNode, useCallback, useRef } from 'react'\nimport { createPortal } from 'react-dom'\n\nexport interface MapTooltipProps {\n  visible: boolean\n  content: ReactNode\n  position?: { x: number; y: number } | null\n  className?: string\n}\n\nconst OFFSET_Y = 10\n\nexport function MapTooltip({\n  visible,\n  content,\n  position,\n  className,\n}: MapTooltipProps) {\n  const ref = useRef<HTMLDivElement>(null)\n\n  const clamp = useCallback(\n    (node: HTMLDivElement | null) => {\n      ref.current = node\n      if (!node) return\n      const rect = node.getBoundingClientRect()\n      const clampedLeft = Math.min(\n        Math.max(rect.width / 2, node.offsetLeft),\n        window.innerWidth - rect.width / 2\n      )\n      if (clampedLeft !== node.offsetLeft) {\n        node.style.left = `${clampedLeft}px`\n      }\n      if (rect.top < 0) {\n        node.style.transform = 'translate(-50%, 0)'\n        node.style.top = `${(position?.y ?? 0) + OFFSET_Y}px`\n      }\n    },\n    [position]\n  )\n\n  if (typeof document === 'undefined' || !position) {\n    return null\n  }\n\n  return createPortal(\n    <div\n      ref={clamp}\n      data-slot='map-tooltip'\n      aria-hidden={!visible}\n      className={cn(\n        'pointer-events-none fixed z-50 rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium shadow-xl transition-opacity duration-150 ease-out',\n        visible ? 'opacity-100' : 'opacity-0',\n        className\n      )}\n      style={{\n        left: position.x,\n        top: position.y - OFFSET_Y,\n        transform: 'translate(-50%, -100%)',\n      }}\n    >\n      {content}\n    </div>,\n    document.body\n  )\n}\n",
      "type": "registry:file",
      "target": "components/shadcnmaps/map-tooltip.tsx"
    },
    {
      "path": "registry/shadcnmaps/map-marker.tsx",
      "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport type { KeyboardEvent, ReactNode } from 'react'\n\nimport type { MarkerEvent } from './types'\n\nexport interface MapMarkerProps {\n  id: string\n  x: number\n  y: number\n  content: ReactNode\n  label?: string\n  disabled?: boolean\n  className?: string\n  onClick?: (event: MarkerEvent) => void\n  onEnter?: (event: MarkerEvent) => void\n  onLeave?: (event: MarkerEvent) => void\n  onMove?: (event: MarkerEvent) => void\n}\n\nexport function MapMarker({\n  id,\n  x,\n  y,\n  content,\n  label,\n  disabled,\n  className,\n  onClick,\n  onEnter,\n  onLeave,\n  onMove,\n}: MapMarkerProps) {\n  const handleKeyDown = (event: KeyboardEvent<SVGGElement>) => {\n    if (disabled) {\n      return\n    }\n\n    if (event.key === 'Enter' || event.key === ' ') {\n      event.preventDefault()\n      onClick?.({\n        id,\n        nativeEvent: event as unknown as MarkerEvent['nativeEvent'],\n      })\n    }\n  }\n\n  return (\n    <g\n      data-slot='map-marker'\n      transform={`translate(${x}, ${y})`}\n      role='button'\n      tabIndex={disabled ? -1 : 0}\n      aria-label={label ?? `Map marker ${id}`}\n      aria-disabled={disabled || undefined}\n      className={cn(\n        'cursor-pointer outline-none motion-safe:transition-transform motion-safe:duration-150',\n        disabled && 'cursor-not-allowed opacity-60',\n        className\n      )}\n      onClick={(nativeEvent) => {\n        if (disabled) {\n          return\n        }\n\n        onClick?.({ id, nativeEvent })\n      }}\n      onMouseEnter={(nativeEvent) => {\n        if (disabled) {\n          return\n        }\n\n        onEnter?.({ id, nativeEvent })\n      }}\n      onMouseLeave={(nativeEvent) => {\n        if (disabled) {\n          return\n        }\n\n        onLeave?.({ id, nativeEvent })\n      }}\n      onMouseMove={(nativeEvent) => {\n        if (disabled) {\n          return\n        }\n\n        onMove?.({ id, nativeEvent })\n      }}\n      onFocus={(nativeEvent) => {\n        if (disabled) {\n          return\n        }\n\n        onEnter?.({\n          id,\n          nativeEvent: nativeEvent as unknown as MarkerEvent['nativeEvent'],\n        })\n      }}\n      onBlur={(nativeEvent) => {\n        if (disabled) {\n          return\n        }\n\n        onLeave?.({\n          id,\n          nativeEvent: nativeEvent as unknown as MarkerEvent['nativeEvent'],\n        })\n      }}\n      onKeyDown={handleKeyDown}\n    >\n      {content}\n    </g>\n  )\n}\n",
      "type": "registry:file",
      "target": "components/shadcnmaps/map-marker.tsx"
    },
    {
      "path": "registry/shadcnmaps/map-listbox.tsx",
      "content": "'use client'\n\nimport { useEffect, useId, useRef } from 'react'\n\nimport { useMapContext } from './map-context'\nimport type { MapRegionData } from './types'\n\nexport interface MapListboxProps {\n  regions: MapRegionData[]\n  disabledRegions: Set<string>\n  label: string\n  onSelect: (region: MapRegionData) => void\n}\n\nexport function MapListbox({\n  regions,\n  disabledRegions,\n  label,\n  onSelect,\n}: MapListboxProps) {\n  const { selectedRegion, setSelectedRegion, focusedRegion, setFocusedRegion } =\n    useMapContext()\n  const typeaheadRef = useRef('')\n  const typeaheadTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const optionRefs = useRef(new Map<string, HTMLDivElement>())\n  const listboxRef = useRef<HTMLDivElement>(null)\n  const id = useId()\n\n  useEffect(() => {\n    return () => {\n      if (typeaheadTimer.current) clearTimeout(typeaheadTimer.current)\n    }\n  }, [])\n\n  const activeRegions = regions.filter((r) => !disabledRegions.has(r.id))\n  const focusedIndex = activeRegions.findIndex((r) => r.id === focusedRegion)\n\n  // The option that should be tabbable (tabIndex=0)\n  const focusableId =\n    focusedRegion && !disabledRegions.has(focusedRegion)\n      ? focusedRegion\n      : selectedRegion && !disabledRegions.has(selectedRegion)\n        ? selectedRegion\n        : activeRegions[0]?.id\n\n  // Move DOM focus when focusedRegion changes\n  useEffect(() => {\n    if (!focusedRegion) return\n    const el = optionRefs.current.get(focusedRegion)\n    if (el && document.activeElement !== el) {\n      el.focus()\n    }\n  }, [focusedRegion])\n\n  // If focusedRegion becomes disabled, move to nearest active region\n  useEffect(() => {\n    if (!focusedRegion) return\n    if (!disabledRegions.has(focusedRegion)) return\n\n    // Find nearest active region\n    const allIndex = regions.findIndex((r) => r.id === focusedRegion)\n    if (allIndex < 0) {\n      setFocusedRegion(activeRegions[0]?.id ?? null)\n      return\n    }\n\n    // Search forward then backward for nearest active\n    for (let offset = 1; offset < regions.length; offset++) {\n      const fwd = regions[(allIndex + offset) % regions.length]\n      if (fwd && !disabledRegions.has(fwd.id)) {\n        setFocusedRegion(fwd.id)\n        return\n      }\n    }\n    setFocusedRegion(null)\n  }, [focusedRegion, disabledRegions, regions, activeRegions, setFocusedRegion])\n\n  function moveTo(index: number) {\n    const region = activeRegions[index]\n    if (region) setFocusedRegion(region.id)\n  }\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n    const idx = focusedIndex < 0 ? 0 : focusedIndex\n\n    switch (event.key) {\n      case 'ArrowDown':\n      case 'ArrowRight':\n        event.preventDefault()\n        moveTo(focusedIndex < activeRegions.length - 1 ? focusedIndex + 1 : 0)\n        break\n\n      case 'ArrowUp':\n      case 'ArrowLeft':\n        event.preventDefault()\n        moveTo(focusedIndex > 0 ? focusedIndex - 1 : activeRegions.length - 1)\n        break\n\n      case 'Home':\n        event.preventDefault()\n        moveTo(0)\n        break\n\n      case 'End':\n        event.preventDefault()\n        moveTo(activeRegions.length - 1)\n        break\n\n      case 'Enter':\n      case ' ': {\n        event.preventDefault()\n        const region = activeRegions[idx]\n        if (!region) break\n        setSelectedRegion((prev) => (prev === region.id ? null : region.id))\n        onSelect(region)\n        break\n      }\n\n      case 'Escape': {\n        event.preventDefault()\n        const selected = selectedRegion\n          ? activeRegions.find((r) => r.id === selectedRegion)\n          : undefined\n        setSelectedRegion(null)\n        if (selected) onSelect(selected)\n        break\n      }\n\n      default:\n        if (event.key.length === 1) {\n          typeaheadRef.current += event.key.toLowerCase()\n          if (typeaheadTimer.current) clearTimeout(typeaheadTimer.current)\n          typeaheadTimer.current = setTimeout(() => {\n            typeaheadRef.current = ''\n          }, 500)\n          const query = typeaheadRef.current\n          const match = activeRegions.findIndex((r) =>\n            r.name.toLowerCase().startsWith(query)\n          )\n          if (match >= 0) moveTo(match)\n        }\n    }\n  }\n\n  function handleBlur(event: React.FocusEvent<HTMLDivElement>) {\n    // Only clear focus when focus leaves the listbox entirely\n    if (\n      !listboxRef.current ||\n      !event.relatedTarget ||\n      !listboxRef.current.contains(event.relatedTarget as Node)\n    ) {\n      setFocusedRegion(null)\n    }\n  }\n\n  return (\n    <div\n      ref={listboxRef}\n      data-slot='map-listbox'\n      role='listbox'\n      aria-label={label}\n      tabIndex={-1}\n      className='sr-only'\n      onKeyDown={handleKeyDown}\n      onBlur={handleBlur}\n    >\n      {regions.map((region) => {\n        const isDisabled = disabledRegions.has(region.id)\n        return (\n          <div\n            key={region.id}\n            id={`${id}-${region.id}`}\n            ref={(el) => {\n              if (el) {\n                optionRefs.current.set(region.id, el)\n              } else {\n                optionRefs.current.delete(region.id)\n              }\n            }}\n            role='option'\n            aria-selected={selectedRegion === region.id}\n            aria-disabled={isDisabled || undefined}\n            tabIndex={\n              isDisabled ? undefined : region.id === focusableId ? 0 : -1\n            }\n            onFocus={isDisabled ? undefined : () => setFocusedRegion(region.id)}\n          >\n            {region.name}\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n",
      "type": "registry:file",
      "target": "components/shadcnmaps/map-listbox.tsx"
    },
    {
      "path": "registry/shadcnmaps/map-controls.tsx",
      "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { useCallback } from 'react'\n\nimport { useMapContext } from './map-context'\n\nexport interface MapControlsProps {\n  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n  showPanButtons?: boolean\n  className?: string\n}\n\nconst positionClasses: Record<string, 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-2 right-2',\n}\n\nfunction ControlButton({\n  onClick,\n  disabled,\n  'aria-label': ariaLabel,\n  children,\n}: {\n  onClick: () => void\n  disabled?: boolean\n  'aria-label': string\n  children: React.ReactNode\n}) {\n  return (\n    <button\n      type='button'\n      onClick={onClick}\n      disabled={disabled}\n      aria-label={ariaLabel}\n      className='flex h-8 w-8 items-center justify-center rounded-md border border-border bg-background text-foreground shadow-sm transition-colors hover:bg-accent disabled:pointer-events-none disabled:opacity-50'\n    >\n      {children}\n    </button>\n  )\n}\n\nfunction PlusIcon() {\n  return (\n    <svg\n      width='20'\n      height='20'\n      viewBox='0 0 24 24'\n      fill='none'\n      stroke='currentColor'\n      strokeWidth='2'\n      strokeLinecap='round'\n    >\n      <path d='M5 12h14' />\n      <path d='M12 5v14' />\n    </svg>\n  )\n}\n\nfunction MinusIcon() {\n  return (\n    <svg\n      width='20'\n      height='20'\n      viewBox='0 0 24 24'\n      fill='none'\n      stroke='currentColor'\n      strokeWidth='2'\n      strokeLinecap='round'\n    >\n      <path d='M5 12h14' />\n    </svg>\n  )\n}\n\nfunction HomeIcon() {\n  return (\n    <svg\n      width='18'\n      height='18'\n      viewBox='0 0 24 24'\n      fill='none'\n      stroke='currentColor'\n      strokeWidth='2'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n    >\n      <path d='M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8' />\n      <path d='M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z' />\n    </svg>\n  )\n}\n\nfunction ChevronUp() {\n  return (\n    <svg\n      width='24'\n      height='24'\n      viewBox='0 0 24 24'\n      fill='none'\n      stroke='currentColor'\n      strokeWidth='2'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n    >\n      <path d='m18 15-6-6-6 6' />\n    </svg>\n  )\n}\n\nfunction ChevronDown() {\n  return (\n    <svg\n      width='24'\n      height='24'\n      viewBox='0 0 24 24'\n      fill='none'\n      stroke='currentColor'\n      strokeWidth='2'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n    >\n      <path d='m6 9 6 6 6-6' />\n    </svg>\n  )\n}\n\nfunction ChevronLeft() {\n  return (\n    <svg\n      width='24'\n      height='24'\n      viewBox='0 0 24 24'\n      fill='none'\n      stroke='currentColor'\n      strokeWidth='2'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n    >\n      <path d='m15 18-6-6 6-6' />\n    </svg>\n  )\n}\n\nfunction ChevronRight() {\n  return (\n    <svg\n      width='24'\n      height='24'\n      viewBox='0 0 24 24'\n      fill='none'\n      stroke='currentColor'\n      strokeWidth='2'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n    >\n      <path d='m9 18 6-6-6-6' />\n    </svg>\n  )\n}\n\nexport function MapControls({\n  position = 'top-right',\n  showPanButtons = true,\n  className,\n}: MapControlsProps) {\n  const { zoomIn, zoomOut, resetZoom, zoomState, zoomConfig, setZoomState } =\n    useMapContext()\n\n  const panUp = useCallback(() => {\n    setZoomState((prev) => ({\n      ...prev,\n      translateY: prev.translateY + zoomConfig.panStep,\n    }))\n  }, [setZoomState, zoomConfig.panStep])\n\n  const panDown = useCallback(() => {\n    setZoomState((prev) => ({\n      ...prev,\n      translateY: prev.translateY - zoomConfig.panStep,\n    }))\n  }, [setZoomState, zoomConfig.panStep])\n\n  const panLeft = useCallback(() => {\n    setZoomState((prev) => ({\n      ...prev,\n      translateX: prev.translateX + zoomConfig.panStep,\n    }))\n  }, [setZoomState, zoomConfig.panStep])\n\n  const panRight = useCallback(() => {\n    setZoomState((prev) => ({\n      ...prev,\n      translateX: prev.translateX - zoomConfig.panStep,\n    }))\n  }, [setZoomState, zoomConfig.panStep])\n\n  const atMaxZoom = zoomState.scale >= zoomConfig.maxZoom\n  const atMinZoom = zoomState.scale <= zoomConfig.minZoom\n\n  return (\n    <div\n      data-slot='map-controls'\n      className={cn(\n        'absolute z-10 flex flex-col items-center gap-1',\n        positionClasses[position],\n        className\n      )}\n    >\n      <ControlButton onClick={zoomIn} disabled={atMaxZoom} aria-label='Zoom in'>\n        <PlusIcon />\n      </ControlButton>\n      <ControlButton\n        onClick={zoomOut}\n        disabled={atMinZoom}\n        aria-label='Zoom out'\n      >\n        <MinusIcon />\n      </ControlButton>\n      <ControlButton onClick={resetZoom} aria-label='Reset zoom'>\n        <HomeIcon />\n      </ControlButton>\n      {showPanButtons ? (\n        <>\n          <div className='my-0.5 h-px w-6 bg-border' />\n          <ControlButton onClick={panUp} aria-label='Pan up'>\n            <ChevronUp />\n          </ControlButton>\n          <div className='flex gap-1'>\n            <ControlButton onClick={panLeft} aria-label='Pan left'>\n              <ChevronLeft />\n            </ControlButton>\n            <ControlButton onClick={panRight} aria-label='Pan right'>\n              <ChevronRight />\n            </ControlButton>\n          </div>\n          <ControlButton onClick={panDown} aria-label='Pan down'>\n            <ChevronDown />\n          </ControlButton>\n        </>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:file",
      "target": "components/shadcnmaps/map-controls.tsx"
    },
    {
      "path": "registry/shadcnmaps/types.ts",
      "content": "import type {\n  FocusEvent,\n  KeyboardEvent,\n  MouseEvent,\n  ReactNode,\n  TouchEvent,\n} from 'react'\n\n/** Unique identifier for the region (e.g., \"AL\", \"CA\", \"FR-IDF\") */\nexport interface MapRegionData {\n  id: string\n  /** Human-readable display name */\n  name: string\n  /** SVG path d attribute */\n  path: string\n  /** Short abbreviation (e.g., \"AL\") */\n  abbreviation?: string\n  /** X coordinate for the abbreviation label within the SVG viewBox */\n  labelX?: number\n  /** Y coordinate for the abbreviation label within the SVG viewBox */\n  labelY?: number\n  /** Arbitrary metadata attached to the region, passed back in event callbacks */\n  metadata?: Record<string, unknown>\n}\n\nexport interface MapData {\n  /** Unique map identifier (e.g., \"usa\", \"world\", \"france\") */\n  id: string\n  /** Human-readable map name */\n  name: string\n  /** SVG viewBox string (e.g., \"0 0 930 590\") */\n  viewBox: string\n  /** Array of region path data */\n  regions: MapRegionData[]\n}\n\nexport interface MapMarkerData {\n  /** Unique identifier */\n  id: string\n  /** X coordinate in the map's SVG coordinate system */\n  x: number\n  /** Y coordinate in the map's SVG coordinate system */\n  y: number\n  /** SVG content to render at (x, y) */\n  content: ReactNode\n  /** Tooltip content shown on hover/click */\n  tooltipContent?: ReactNode\n  /** Accessible label */\n  label?: string\n  /** When true, the marker is not interactive */\n  disabled?: boolean\n}\n\nexport interface RegionEvent {\n  /** The region's static data from MapRegionData */\n  region: MapRegionData\n  /** The native React synthetic event (null for programmatic selection via keyboard listbox) */\n  nativeEvent:\n    | MouseEvent<SVGPathElement>\n    | TouchEvent<SVGPathElement>\n    | FocusEvent<SVGPathElement>\n    | KeyboardEvent<SVGPathElement>\n    | null\n}\n\nexport interface MarkerEvent {\n  /** The marker's id */\n  id: string\n  /** The native React synthetic event */\n  nativeEvent:\n    | MouseEvent<SVGGElement>\n    | TouchEvent<SVGGElement>\n    | FocusEvent<SVGGElement>\n    | KeyboardEvent<SVGGElement>\n}\n\nexport interface RegionOverride {\n  id: string\n  /** Override display name (e.g. for localization) */\n  name?: string\n  /** Override abbreviation label */\n  abbreviation?: string\n  /** Override label X position */\n  labelX?: number\n  /** Override label Y position */\n  labelY?: number\n  /** Attach arbitrary metadata */\n  metadata?: Record<string, unknown>\n  /** Extra CSS classes for fill/stroke */\n  className?: string\n  /** Extra CSS classes for the abbreviation label */\n  labelClassName?: string\n  /** Custom tooltip content */\n  tooltipContent?: ReactNode\n  /** Disable interaction */\n  disabled?: boolean\n}\n\nexport interface ZoomState {\n  scale: number\n  translateX: number\n  translateY: number\n}\n\nexport interface ZoomConfig {\n  minZoom: number\n  maxZoom: number\n  zoomStep: number\n  panStep: number\n}\n",
      "type": "registry:file",
      "target": "components/shadcnmaps/types.ts"
    }
  ],
  "cssVars": {
    "light": {
      "map-region": "oklch(44.3% 0.11 240.79)",
      "map-region-hover": "oklch(50% 0.134 242.748)",
      "map-region-selected": "oklch(39.1% 0.09 240.875)",
      "map-region-disabled": "oklch(55.4% 0.046 257.417)",
      "map-region-stroke": "oklch(28.2% 0.091 267.935)",
      "map-region-stroke-hover": "oklch(28.2% 0.091 267.935)",
      "map-region-focus-ring": "oklch(70.7% 0.165 254.624)",
      "map-label": "oklch(100% 0 0)",
      "map-label-hover": "oklch(100% 0 0)",
      "map-label-selected": "oklch(100% 0 0)"
    },
    "dark": {
      "map-region": "oklch(44.3% 0.11 240.79)",
      "map-region-hover": "oklch(50% 0.134 242.748)",
      "map-region-selected": "oklch(39.1% 0.09 240.875)",
      "map-region-disabled": "oklch(55.4% 0.046 257.417)",
      "map-region-stroke": "oklch(28.2% 0.091 267.935)",
      "map-region-stroke-hover": "oklch(28.2% 0.091 267.935)",
      "map-region-focus-ring": "oklch(70.7% 0.165 254.624)",
      "map-label": "oklch(100% 0 0)",
      "map-label-hover": "oklch(100% 0 0)",
      "map-label-selected": "oklch(100% 0 0)"
    }
  },
  "type": "registry:ui"
}