{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "choropleth-chart",
  "type": "registry:component",
  "title": "Choropleth Map",
  "description": "A geographic map chart with zoom, pan, and data visualization",
  "dependencies": [
    "@types/geojson",
    "@visx/geo@4.0.1-alpha.0",
    "@visx/responsive@4.0.1-alpha.0",
    "@visx/zoom@4.0.1-alpha.0",
    "d3-geo",
    "topojson-client",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-animation",
    "@bklit/utils",
    "@bklit/chart-utils",
    "@bklit/chart-tooltip"
  ],
  "files": [
    {
      "path": "src/charts/choropleth/choropleth-chart.tsx",
      "content": "\"use client\";\n\nimport { Mercator } from \"@visx/geo\";\nimport { ParentSize } from \"@visx/responsive\";\nimport type { TransformMatrix } from \"@visx/zoom\";\nimport { Zoom } from \"@visx/zoom\";\nimport type { FeatureCollection, Geometry } from \"geojson\";\nimport type { Transition } from \"motion/react\";\nimport React, {\n  memo,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  type ChoroplethFeature,\n  type ChoroplethFeatureProperties,\n  ChoroplethInteractionShell,\n  ChoroplethStableProvider,\n  ChoroplethZoomContext,\n  type Margin,\n  useChoroplethInteraction,\n  type ZoomInstance,\n} from \"./choropleth-context\";\nimport { ChoroplethFeature as ChoroplethFeatureLayer } from \"./choropleth-feature\";\nimport { ChoroplethGraticule as ChoroplethGraticuleLayer } from \"./choropleth-graticule\";\nimport { ChoroplethTooltip as ChoroplethTooltipLayer } from \"./choropleth-tooltip\";\n\nexport interface ChoroplethChartProps {\n  /** GeoJSON FeatureCollection data */\n  data: FeatureCollection<Geometry, ChoroplethFeatureProperties>;\n  /** Chart margins */\n  margin?: Partial<Margin>;\n  /** Animation duration in milliseconds. Default: 800 */\n  animationDuration?: number;\n  /** Motion enter transition (spring or cubic-bezier tween). */\n  enterTransition?: Transition;\n  /** Signature of motion URL state — triggers enter replay when it changes. */\n  revealSignature?: string;\n  /** Aspect ratio as \"width / height\". Default: \"16 / 9\" */\n  aspectRatio?: string;\n  /** Projection scale. If not provided, auto-calculated based on width */\n  scale?: number;\n  /** Center coordinates [longitude, latitude]. Default: [0, 20] */\n  center?: [number, number];\n  /** Translate offset [x, y]. If not provided, auto-calculated to center */\n  translate?: [number, number];\n  /** Enable zoom and pan. Default: false */\n  zoomEnabled?: boolean;\n  /** Minimum zoom scale. Default: 0.5 */\n  zoomMin?: number;\n  /** Maximum zoom scale. Default: 4 */\n  zoomMax?: number;\n  /** Initial zoom transform */\n  initialZoom?: TransformMatrix;\n  /** Additional class name for the container */\n  className?: string;\n  /** Child components (ChoroplethFeature, ChoroplethGraticule, ChoroplethTooltip) */\n  children: ReactNode;\n}\n\nconst DEFAULT_MARGIN: Margin = { top: 0, right: 0, bottom: 0, left: 0 };\n\n// Known SVG component displayNames\nconst SVG_COMPONENT_NAMES = new Set([\n  \"ChoroplethFeature\",\n  \"ChoroplethGraticule\",\n  \"ChoroplethTooltip\",\n]);\n\nconst SVG_COMPONENT_TYPES = new Set([\n  ChoroplethFeatureLayer,\n  ChoroplethGraticuleLayer,\n  ChoroplethTooltipLayer,\n]);\n\nfunction resolveComponentType(type: unknown): unknown {\n  if (\n    typeof type === \"object\" &&\n    type !== null &&\n    \"type\" in type &&\n    (type as { type?: unknown }).type\n  ) {\n    return (type as { type: unknown }).type;\n  }\n  return type;\n}\n\nfunction getComponentDisplayName(type: unknown): string | null {\n  if (typeof type === \"function\") {\n    const fn = type as { displayName?: string; name?: string };\n    return fn.displayName ?? fn.name ?? null;\n  }\n  if (typeof type === \"object\" && type !== null) {\n    const wrapped = type as {\n      displayName?: string;\n      type?: { displayName?: string; name?: string };\n    };\n    if (wrapped.displayName) {\n      return wrapped.displayName;\n    }\n    const inner = wrapped.type;\n    if (typeof inner === \"function\") {\n      const innerFn = inner as { displayName?: string; name?: string };\n      return innerFn.displayName ?? innerFn.name ?? null;\n    }\n  }\n  return null;\n}\n\nfunction isChoroplethSvgChild(type: unknown): boolean {\n  if (SVG_COMPONENT_TYPES.has(type as never)) {\n    return true;\n  }\n  const resolved = resolveComponentType(type);\n  if (resolved !== type && SVG_COMPONENT_TYPES.has(resolved as never)) {\n    return true;\n  }\n  const displayName = getComponentDisplayName(type);\n  return displayName !== null && SVG_COMPONENT_NAMES.has(displayName);\n}\n\n// HTML elements that should render in overlay layer\nconst HTML_ELEMENTS = new Set([\"div\", \"span\", \"button\", \"p\", \"a\"]);\n\n// Separate children into SVG and overlay layers\nfunction separateChildren(children: ReactNode): {\n  svgChildren: React.ReactNode[];\n  overlayChildren: React.ReactNode[];\n} {\n  const childArray = React.Children.toArray(children);\n  const svgChildren: React.ReactNode[] = [];\n  const overlayChildren: React.ReactNode[] = [];\n\n  for (const child of childArray) {\n    if (!React.isValidElement(child)) {\n      svgChildren.push(child);\n      continue;\n    }\n\n    if (isChoroplethSvgChild(child.type)) {\n      svgChildren.push(child);\n    } else if (typeof child.type === \"string\") {\n      if (HTML_ELEMENTS.has(child.type)) {\n        overlayChildren.push(child);\n      } else {\n        svgChildren.push(child);\n      }\n    } else {\n      overlayChildren.push(child);\n    }\n  }\n\n  return { svgChildren, overlayChildren };\n}\n\nconst DEFAULT_INITIAL_ZOOM: TransformMatrix = {\n  scaleX: 1,\n  scaleY: 1,\n  translateX: 0,\n  translateY: 0,\n  skewX: 0,\n  skewY: 0,\n};\n\ninterface MercatorRenderProps {\n  // biome-ignore lint/suspicious/noExplicitAny: visx geo projection bundle\n  path: (geo: any) => string | null;\n  projection: (coords: [number, number]) => [number, number] | null | undefined;\n}\n\ninterface ChoroplethMercatorContentProps {\n  mercator: MercatorRenderProps;\n  data: FeatureCollection<Geometry, ChoroplethFeatureProperties>;\n  width: number;\n  height: number;\n  innerWidth: number;\n  innerHeight: number;\n  margin: Margin;\n  animationDuration: number;\n  enterTransition?: Transition;\n  revealEpoch: number;\n  isLoaded: boolean;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  svgChildren: React.ReactNode[];\n  overlayChildren: React.ReactNode[];\n  zoom?: ZoomInstance<SVGSVGElement>;\n}\n\nconst ChoroplethSvg = memo(function ChoroplethSvg({\n  height,\n  width,\n  svgChildren,\n  zoom,\n}: {\n  height: number;\n  width: number;\n  svgChildren: React.ReactNode[];\n  zoom?: ZoomInstance<SVGSVGElement>;\n}) {\n  const { setHoveredFeatureIndex, setTooltipData } = useChoroplethInteraction();\n\n  const handleMouseLeave = useCallback(() => {\n    setHoveredFeatureIndex(null);\n    setTooltipData(null);\n  }, [setHoveredFeatureIndex, setTooltipData]);\n\n  return (\n    <svg\n      aria-hidden=\"true\"\n      height={height}\n      onMouseLeave={handleMouseLeave}\n      ref={zoom?.containerRef}\n      style={{\n        contain: \"layout style paint\",\n        cursor: zoom?.isDragging ? \"grabbing\" : \"grab\",\n        touchAction: \"none\",\n      }}\n      width={width}\n    >\n      <g\n        style={{\n          transition: zoom?.isDragging ? \"none\" : \"transform 0.18s ease-out\",\n        }}\n        transform={zoom ? zoom.toString() : undefined}\n      >\n        {svgChildren}\n      </g>\n    </svg>\n  );\n});\n\nconst ChoroplethMercatorContent = memo(function ChoroplethMercatorContent({\n  mercator,\n  data,\n  width,\n  height,\n  innerWidth,\n  innerHeight,\n  margin,\n  animationDuration,\n  enterTransition,\n  revealEpoch,\n  isLoaded,\n  containerRef,\n  svgChildren,\n  overlayChildren,\n  zoom,\n}: ChoroplethMercatorContentProps) {\n  const featurePaths = data.features.map(\n    (feature) => mercator.path(feature) ?? null\n  ) as (string | null)[];\n\n  const pathGenerator = useCallback(\n    (feature: ChoroplethFeature) => mercator.path(feature) ?? undefined,\n    [mercator]\n  );\n\n  const rawPathGenerator = useCallback(\n    // biome-ignore lint/suspicious/noExplicitAny: GeoJSON types are complex\n    (geo: any) => mercator.path(geo),\n    [mercator]\n  );\n\n  const projectPoint = useCallback(\n    (coords: [number, number]): [number, number] | null => {\n      const projected = mercator.projection(coords);\n      if (!projected) {\n        return null;\n      }\n      return projected as [number, number];\n    },\n    [mercator]\n  );\n\n  const stableValue = useMemo(\n    () => ({\n      features: data.features,\n      featureCollection: data,\n      featurePaths,\n      pathGenerator,\n      rawPathGenerator,\n      projectPoint,\n      width,\n      height,\n      innerWidth,\n      innerHeight,\n      margin,\n      containerRef,\n      isLoaded,\n      animationDuration,\n      enterTransition,\n      revealEpoch,\n    }),\n    [\n      animationDuration,\n      containerRef,\n      data,\n      enterTransition,\n      featurePaths,\n      height,\n      innerHeight,\n      innerWidth,\n      isLoaded,\n      margin,\n      pathGenerator,\n      projectPoint,\n      rawPathGenerator,\n      revealEpoch,\n      width,\n    ]\n  );\n\n  return (\n    <ChoroplethZoomContext.Provider value={{ zoom: zoom ?? null }}>\n      <ChoroplethStableProvider value={stableValue}>\n        <ChoroplethInteractionShell>\n          <div className=\"relative h-full w-full\" ref={containerRef}>\n            <ChoroplethSvg\n              height={height}\n              svgChildren={svgChildren}\n              width={width}\n              zoom={zoom}\n            />\n            {overlayChildren}\n          </div>\n        </ChoroplethInteractionShell>\n      </ChoroplethStableProvider>\n    </ChoroplethZoomContext.Provider>\n  );\n});\n\nfunction ChoroplethChartInner({\n  data,\n  width,\n  height,\n  margin,\n  animationDuration,\n  enterTransition,\n  revealSignature = \"\",\n  scale: scaleProp,\n  center,\n  translate: translateProp,\n  zoomEnabled,\n  zoomMin,\n  zoomMax,\n  initialZoom,\n  children,\n}: {\n  data: FeatureCollection<Geometry, ChoroplethFeatureProperties>;\n  width: number;\n  height: number;\n  margin: Margin;\n  animationDuration: number;\n  enterTransition?: Transition;\n  revealSignature?: string;\n  scale?: number;\n  center: [number, number];\n  translate?: [number, number];\n  zoomEnabled: boolean;\n  zoomMin: number;\n  zoomMax: number;\n  initialZoom: TransformMatrix;\n  children: ReactNode;\n}) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [isLoaded, setIsLoaded] = useState(false);\n  const [revealEpoch, setRevealEpoch] = useState(0);\n\n  const innerWidth = width - margin.left - margin.right;\n  const innerHeight = height - margin.top - margin.bottom;\n\n  const scale = scaleProp ?? (innerWidth / 630) * 100;\n\n  const translate = translateProp ?? [\n    innerWidth / 2 + margin.left,\n    innerHeight / 2 + margin.top + 50,\n  ];\n\n  const { svgChildren, overlayChildren } = useMemo(\n    () => separateChildren(children),\n    [children]\n  );\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: revealSignature\n  useEffect(() => {\n    setRevealEpoch((n) => n + 1);\n    setIsLoaded(false);\n    const timeout = setTimeout(() => {\n      setIsLoaded(true);\n    }, animationDuration);\n    return () => clearTimeout(timeout);\n  }, [animationDuration, revealSignature]);\n\n  if (width < 10 || height < 10) {\n    return null;\n  }\n\n  const mercatorContentProps = {\n    animationDuration,\n    containerRef,\n    data,\n    enterTransition,\n    height,\n    innerHeight,\n    innerWidth,\n    isLoaded,\n    margin,\n    overlayChildren,\n    revealEpoch,\n    svgChildren,\n    width,\n  };\n\n  return (\n    <Mercator\n      center={center}\n      data={data.features}\n      scale={scale}\n      translate={translate as [number, number]}\n    >\n      {(mercator) => {\n        const content = (zoom?: ZoomInstance<SVGSVGElement>) => (\n          <ChoroplethMercatorContent\n            {...mercatorContentProps}\n            mercator={mercator}\n            zoom={zoom}\n          />\n        );\n\n        if (zoomEnabled) {\n          return (\n            <Zoom<SVGSVGElement>\n              height={height}\n              initialTransformMatrix={initialZoom}\n              scaleXMax={zoomMax}\n              scaleXMin={zoomMin}\n              scaleYMax={zoomMax}\n              scaleYMin={zoomMin}\n              wheelDelta={(event) => {\n                const zoomScale = event.deltaY > 0 ? 0.95 : 1.05;\n                return { scaleX: zoomScale, scaleY: zoomScale };\n              }}\n              width={width}\n            >\n              {(zoom) => content(zoom)}\n            </Zoom>\n          );\n        }\n\n        return content();\n      }}\n    </Mercator>\n  );\n}\n\nexport function ChoroplethChart({\n  data,\n  margin: marginProp,\n  animationDuration = 800,\n  enterTransition,\n  revealSignature,\n  aspectRatio = \"16 / 9\",\n  scale,\n  center = [0, 20],\n  translate,\n  zoomEnabled = false,\n  zoomMin = 0.5,\n  zoomMax = 4,\n  initialZoom = DEFAULT_INITIAL_ZOOM,\n  className = \"\",\n  children,\n}: ChoroplethChartProps) {\n  const margin = { ...DEFAULT_MARGIN, ...marginProp };\n\n  return (\n    <div className={cn(\"relative w-full\", className)} style={{ aspectRatio }}>\n      <ParentSize debounceTime={10}>\n        {({ width, height }) =>\n          width > 0 && height > 0 ? (\n            <ChoroplethChartInner\n              animationDuration={animationDuration}\n              center={center}\n              data={data}\n              enterTransition={enterTransition}\n              height={height}\n              initialZoom={initialZoom}\n              margin={margin}\n              revealSignature={revealSignature}\n              scale={scale}\n              translate={translate}\n              width={width}\n              zoomEnabled={zoomEnabled}\n              zoomMax={zoomMax}\n              zoomMin={zoomMin}\n            >\n              {children}\n            </ChoroplethChartInner>\n          ) : null\n        }\n      </ParentSize>\n    </div>\n  );\n}\n\nChoroplethChart.displayName = \"ChoroplethChart\";\n\nexport default ChoroplethChart;\n",
      "type": "registry:component",
      "target": "components/charts/choropleth/choropleth-chart.tsx"
    },
    {
      "path": "src/charts/choropleth/choropleth-context.tsx",
      "content": "\"use client\";\n\nimport type { ProvidedZoom, TransformMatrix } from \"@visx/zoom\";\nimport type { Feature, FeatureCollection, Geometry } from \"geojson\";\nimport type { Transition } from \"motion/react\";\nimport {\n  createContext,\n  type Dispatch,\n  type RefObject,\n  type SetStateAction,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\n\n// ZoomState from visx/zoom that includes isDragging\ninterface ZoomState {\n  initialTransformMatrix: TransformMatrix;\n  transformMatrix: TransformMatrix;\n  isDragging: boolean;\n}\n\n// Combined type from visx Zoom children prop\nexport type ZoomInstance<E extends Element> = ProvidedZoom<E> & ZoomState;\n\n// Zoom context to share zoom controls with child components\ninterface ChoroplethZoomContextValue {\n  zoom: ZoomInstance<SVGSVGElement> | null;\n}\n\nexport const ChoroplethZoomContext = createContext<ChoroplethZoomContextValue>({\n  zoom: null,\n});\n\nexport function useChoroplethZoom() {\n  return useContext(ChoroplethZoomContext);\n}\n\nexport interface Margin {\n  top: number;\n  right: number;\n  bottom: number;\n  left: number;\n}\n\nexport interface ChoroplethFeatureProperties {\n  name?: string;\n  id?: string | number;\n  [key: string]: unknown;\n}\n\nexport type ChoroplethFeature = Feature<Geometry, ChoroplethFeatureProperties>;\n\nexport interface ChoroplethTooltipData {\n  featureIndex: number;\n  x: number;\n  y: number;\n  feature: ChoroplethFeature;\n}\n\nexport interface ChoroplethInteractionContextValue {\n  hoveredFeatureIndex: number | null;\n  setHoveredFeatureIndex: (index: number | null) => void;\n  tooltipData: ChoroplethTooltipData | null;\n  setTooltipData: Dispatch<SetStateAction<ChoroplethTooltipData | null>>;\n}\n\nexport interface ChoroplethStableContextValue {\n  // Geo data\n  features: ChoroplethFeature[];\n  featureCollection: FeatureCollection<Geometry, ChoroplethFeatureProperties>;\n\n  /** Precomputed SVG path strings — one per feature index. */\n  featurePaths: readonly (string | null)[];\n\n  // Projection function (returns path string)\n  pathGenerator: (feature: ChoroplethFeature) => string | undefined;\n\n  // Raw path function for graticule (accepts any geo object)\n  // biome-ignore lint/suspicious/noExplicitAny: GeoJSON types are complex\n  rawPathGenerator: (geo: any) => string | null;\n\n  // Project geo coordinates to screen coordinates\n  projectPoint: (coords: [number, number]) => [number, number] | null;\n\n  // Dimensions\n  width: number;\n  height: number;\n  innerWidth: number;\n  innerHeight: number;\n  margin: Margin;\n\n  containerRef: RefObject<HTMLDivElement | null>;\n\n  // Animation\n  isLoaded: boolean;\n  animationDuration: number;\n  enterTransition?: Transition;\n  revealEpoch: number;\n}\n\nexport type ChoroplethContextValue = ChoroplethStableContextValue &\n  ChoroplethInteractionContextValue;\n\nconst ChoroplethStableContext =\n  createContext<ChoroplethStableContextValue | null>(null);\nconst ChoroplethInteractionContext =\n  createContext<ChoroplethInteractionContextValue | null>(null);\n\nexport function ChoroplethStableProvider({\n  children,\n  value,\n}: {\n  children: React.ReactNode;\n  value: ChoroplethStableContextValue;\n}) {\n  return (\n    <ChoroplethStableContext.Provider value={value}>\n      {children}\n    </ChoroplethStableContext.Provider>\n  );\n}\n\nexport function ChoroplethInteractionShell({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const [hoveredFeatureIndex, setHoveredFeatureIndex] = useState<number | null>(\n    null\n  );\n  const [tooltipData, setTooltipData] = useState<ChoroplethTooltipData | null>(\n    null\n  );\n\n  const interaction = useMemo<ChoroplethInteractionContextValue>(\n    () => ({\n      hoveredFeatureIndex,\n      setHoveredFeatureIndex,\n      tooltipData,\n      setTooltipData,\n    }),\n    [hoveredFeatureIndex, tooltipData]\n  );\n\n  return (\n    <ChoroplethInteractionContext.Provider value={interaction}>\n      {children}\n    </ChoroplethInteractionContext.Provider>\n  );\n}\n\nexport function ChoroplethProvider({\n  children,\n  value,\n}: {\n  children: React.ReactNode;\n  value: ChoroplethContextValue;\n}) {\n  const stable = useMemo<ChoroplethStableContextValue>(\n    () => ({\n      features: value.features,\n      featureCollection: value.featureCollection,\n      featurePaths: value.featurePaths,\n      pathGenerator: value.pathGenerator,\n      rawPathGenerator: value.rawPathGenerator,\n      projectPoint: value.projectPoint,\n      width: value.width,\n      height: value.height,\n      innerWidth: value.innerWidth,\n      innerHeight: value.innerHeight,\n      margin: value.margin,\n      containerRef: value.containerRef,\n      isLoaded: value.isLoaded,\n      animationDuration: value.animationDuration,\n      enterTransition: value.enterTransition,\n      revealEpoch: value.revealEpoch,\n    }),\n    [\n      value.features,\n      value.featureCollection,\n      value.featurePaths,\n      value.pathGenerator,\n      value.rawPathGenerator,\n      value.projectPoint,\n      value.width,\n      value.height,\n      value.innerWidth,\n      value.innerHeight,\n      value.margin,\n      value.containerRef,\n      value.isLoaded,\n      value.animationDuration,\n      value.enterTransition,\n      value.revealEpoch,\n    ]\n  );\n\n  const interaction = useMemo<ChoroplethInteractionContextValue>(\n    () => ({\n      hoveredFeatureIndex: value.hoveredFeatureIndex,\n      setHoveredFeatureIndex: value.setHoveredFeatureIndex,\n      tooltipData: value.tooltipData,\n      setTooltipData: value.setTooltipData,\n    }),\n    [\n      value.hoveredFeatureIndex,\n      value.setHoveredFeatureIndex,\n      value.tooltipData,\n      value.setTooltipData,\n    ]\n  );\n\n  return (\n    <ChoroplethStableProvider value={stable}>\n      <ChoroplethInteractionContext.Provider value={interaction}>\n        {children}\n      </ChoroplethInteractionContext.Provider>\n    </ChoroplethStableProvider>\n  );\n}\n\nexport function useChoroplethStable(): ChoroplethStableContextValue {\n  const context = useContext(ChoroplethStableContext);\n  if (!context) {\n    throw new Error(\n      \"useChoroplethStable must be used within a ChoroplethProvider\"\n    );\n  }\n  return context;\n}\n\nexport function useChoroplethInteraction(): ChoroplethInteractionContextValue {\n  const context = useContext(ChoroplethInteractionContext);\n  if (!context) {\n    throw new Error(\n      \"useChoroplethInteraction must be used within a ChoroplethProvider\"\n    );\n  }\n  return context;\n}\n\nexport function useChoropleth(): ChoroplethContextValue {\n  return { ...useChoroplethStable(), ...useChoroplethInteraction() };\n}\n\nimport { CHART_SCALE_VARS, chartScaleCssVars } from \"../chart-scale\";\n\n// CSS variables for choropleth theming\nexport const choroplethCssVars = {\n  scale01: chartScaleCssVars.scale01,\n  scale02: chartScaleCssVars.scale02,\n  scale03: chartScaleCssVars.scale03,\n  scale04: chartScaleCssVars.scale04,\n  scale05: chartScaleCssVars.scale05,\n  patternColor: chartScaleCssVars.patternColor,\n  stroke: \"var(--chart-grid)\",\n  background: \"var(--background)\",\n};\n\n// Default colors array for cycling through features\nexport const defaultChoroplethColors = [...CHART_SCALE_VARS];\n",
      "type": "registry:component",
      "target": "components/charts/choropleth/choropleth-context.tsx"
    },
    {
      "path": "src/charts/chart-scale.ts",
      "content": "/** Sequential scale CSS variables for heatmaps, choropleths, and binned data (01 = lowest, 05 = highest). */\nexport const CHART_SCALE_VARS = [\n  \"var(--chart-scale-01)\",\n  \"var(--chart-scale-02)\",\n  \"var(--chart-scale-03)\",\n  \"var(--chart-scale-04)\",\n  \"var(--chart-scale-05)\",\n] as const;\n\nexport type ChartScaleVars = typeof CHART_SCALE_VARS;\n\nexport const chartScaleCssVars = {\n  scale01: CHART_SCALE_VARS[0],\n  scale02: CHART_SCALE_VARS[1],\n  scale03: CHART_SCALE_VARS[2],\n  scale04: CHART_SCALE_VARS[3],\n  scale05: CHART_SCALE_VARS[4],\n  patternColor: \"var(--chart-scale-pattern-color)\",\n} as const;\n",
      "type": "registry:component",
      "target": "components/charts/chart-scale.ts"
    },
    {
      "path": "src/charts/choropleth/choropleth-feature.tsx",
      "content": "\"use client\";\n\nimport { geoCentroid } from \"d3-geo\";\nimport { motion, useTransform } from \"motion/react\";\nimport { memo, useCallback, useMemo } from \"react\";\nimport { useEnterComplete } from \"../use-enter-complete\";\nimport { useMountProgress } from \"../use-mount-progress\";\nimport {\n  type ChoroplethFeature as ChoroplethFeatureType,\n  defaultChoroplethColors,\n  useChoroplethInteraction,\n  useChoroplethStable,\n} from \"./choropleth-context\";\n\nexport interface ChoroplethFeatureProps {\n  fill?: string;\n  stroke?: string;\n  strokeWidth?: number;\n  fadedOpacity?: number;\n  getFeatureColor?: (feature: ChoroplethFeatureType, index: number) => string;\n  patterns?: React.ReactNode;\n  getFeaturePattern?: (\n    feature: ChoroplethFeatureType,\n    index: number\n  ) => string | null | undefined;\n}\n\ninterface FeatureRecord {\n  index: number;\n  path: string;\n  fill: string;\n  feature: ChoroplethFeatureType;\n  centroid: { x: number; y: number } | null;\n}\n\nfunction resolveFeatureFill(\n  feature: ChoroplethFeatureType,\n  index: number,\n  fill: string | undefined,\n  getFeatureColor: ChoroplethFeatureProps[\"getFeatureColor\"],\n  getFeaturePattern: ChoroplethFeatureProps[\"getFeaturePattern\"]\n): string {\n  const patternId = getFeaturePattern?.(feature, index);\n  if (patternId) {\n    return `url(#${patternId})`;\n  }\n  if (fill) {\n    return fill;\n  }\n  if (getFeatureColor) {\n    return getFeatureColor(feature, index);\n  }\n  return (\n    defaultChoroplethColors[index % defaultChoroplethColors.length] ??\n    \"var(--chart-1)\"\n  );\n}\n\nconst StaticFeatureLayer = memo(function StaticFeatureLayer({\n  records,\n  stroke,\n  strokeWidth,\n  baseOpacity,\n  dimOpacity,\n  hoveredIndex,\n  onFeatureEnter,\n  onFeatureLeave,\n}: {\n  records: FeatureRecord[];\n  stroke: string;\n  strokeWidth: number;\n  baseOpacity: number;\n  dimOpacity: number;\n  hoveredIndex: number | null;\n  onFeatureEnter: (record: FeatureRecord) => void;\n  onFeatureLeave: () => void;\n}) {\n  const isDimmed = hoveredIndex !== null;\n\n  if (!isDimmed) {\n    return (\n      <g opacity={baseOpacity}>\n        {records.map((record) => (\n          // biome-ignore lint/a11y/noStaticElementInteractions: SVG path used as hover hitbox\n          <path\n            className=\"cursor-pointer\"\n            d={record.path}\n            fill={record.fill}\n            key={`base-${record.index}`}\n            onMouseEnter={() => onFeatureEnter(record)}\n            onMouseLeave={onFeatureLeave}\n            stroke={stroke}\n            strokeWidth={strokeWidth}\n          />\n        ))}\n      </g>\n    );\n  }\n\n  const highlighted = records.find((record) => record.index === hoveredIndex);\n\n  return (\n    <>\n      <g opacity={dimOpacity} style={{ transition: \"opacity 0.18s ease-out\" }}>\n        {records\n          .filter((record) => record.index !== hoveredIndex)\n          .map((record) => (\n            // biome-ignore lint/a11y/noStaticElementInteractions: SVG path used as hover hitbox\n            <path\n              className=\"cursor-pointer\"\n              d={record.path}\n              fill={record.fill}\n              key={`base-${record.index}`}\n              onMouseEnter={() => onFeatureEnter(record)}\n              onMouseLeave={onFeatureLeave}\n              stroke={stroke}\n              strokeWidth={strokeWidth}\n            />\n          ))}\n      </g>\n      {highlighted ? (\n        // biome-ignore lint/a11y/noStaticElementInteractions: SVG path used as hover hitbox\n        <path\n          className=\"cursor-pointer\"\n          d={highlighted.path}\n          fill={highlighted.fill}\n          key={`highlight-${highlighted.index}`}\n          onMouseEnter={() => onFeatureEnter(highlighted)}\n          onMouseLeave={onFeatureLeave}\n          opacity={1}\n          stroke={stroke}\n          strokeWidth={strokeWidth}\n          style={{ transition: \"opacity 0.18s ease-out\" }}\n        />\n      ) : null}\n    </>\n  );\n});\n\nconst EnterFeatureLayer = memo(function EnterFeatureLayer({\n  records,\n  stroke,\n  strokeWidth,\n  baseOpacity,\n  dimOpacity,\n  hoveredIndex,\n  onFeatureEnter,\n  onFeatureLeave,\n  revealEpoch,\n}: {\n  records: FeatureRecord[];\n  stroke: string;\n  strokeWidth: number;\n  baseOpacity: number;\n  dimOpacity: number;\n  hoveredIndex: number | null;\n  onFeatureEnter: (record: FeatureRecord) => void;\n  onFeatureLeave: () => void;\n  revealEpoch: number;\n}) {\n  const { enterTransition, animationDuration } = useChoroplethStable();\n  const mountProgress = useMountProgress(\n    enterTransition,\n    0,\n    `choropleth-layer-${revealEpoch}`\n  );\n  const enterComplete = useEnterComplete(mountProgress);\n  const layerOpacity = useTransform(mountProgress, (t) => t * baseOpacity);\n\n  if (enterComplete) {\n    return (\n      <StaticFeatureLayer\n        baseOpacity={baseOpacity}\n        dimOpacity={dimOpacity}\n        hoveredIndex={hoveredIndex}\n        onFeatureEnter={onFeatureEnter}\n        onFeatureLeave={onFeatureLeave}\n        records={records}\n        stroke={stroke}\n        strokeWidth={strokeWidth}\n      />\n    );\n  }\n\n  return (\n    <motion.g\n      key={`enter-${revealEpoch}`}\n      opacity={layerOpacity}\n      transition={{\n        duration: animationDuration / 1000,\n        ease: \"easeOut\",\n      }}\n    >\n      {records.map((record) => (\n        // biome-ignore lint/a11y/noStaticElementInteractions: SVG path used as hover hitbox\n        <path\n          className=\"cursor-pointer\"\n          d={record.path}\n          fill={record.fill}\n          key={`enter-${record.index}`}\n          onMouseEnter={() => onFeatureEnter(record)}\n          onMouseLeave={onFeatureLeave}\n          stroke={stroke}\n          strokeWidth={strokeWidth}\n        />\n      ))}\n    </motion.g>\n  );\n});\n\nexport const ChoroplethFeature = memo(function ChoroplethFeature({\n  fill,\n  stroke = \"var(--background)\",\n  strokeWidth = 0.5,\n  fadedOpacity = 0.4,\n  getFeatureColor,\n  patterns,\n  getFeaturePattern,\n}: ChoroplethFeatureProps) {\n  const {\n    features,\n    featurePaths,\n    pathGenerator,\n    projectPoint,\n    isLoaded,\n    revealEpoch,\n    width,\n    height,\n  } = useChoroplethStable();\n  const { hoveredFeatureIndex, setHoveredFeatureIndex, setTooltipData } =\n    useChoroplethInteraction();\n\n  const featureCentroids = useMemo(() => {\n    return features.map((feature) => {\n      try {\n        const centroid = geoCentroid(feature);\n        if (\n          centroid &&\n          !Number.isNaN(centroid[0]) &&\n          !Number.isNaN(centroid[1])\n        ) {\n          const projected = projectPoint(centroid as [number, number]);\n          if (projected) {\n            const padding = 60;\n            return {\n              x: Math.max(padding, Math.min(width - padding, projected[0])),\n              y: Math.max(padding, Math.min(height - padding, projected[1])),\n            };\n          }\n        }\n      } catch {\n        // Some geometries may not have valid centroids\n      }\n      return null;\n    });\n  }, [features, projectPoint, width, height]);\n\n  const records = useMemo(() => {\n    const items: FeatureRecord[] = [];\n    for (let index = 0; index < features.length; index++) {\n      const feature = features[index];\n      if (!feature) {\n        continue;\n      }\n\n      const path = featurePaths[index] ?? pathGenerator(feature);\n      if (!path) {\n        continue;\n      }\n\n      items.push({\n        index,\n        path,\n        fill: resolveFeatureFill(\n          feature,\n          index,\n          fill,\n          getFeatureColor,\n          getFeaturePattern\n        ),\n        feature,\n        centroid: featureCentroids[index] ?? null,\n      });\n    }\n    return items;\n  }, [\n    featureCentroids,\n    featurePaths,\n    features,\n    fill,\n    getFeatureColor,\n    getFeaturePattern,\n    pathGenerator,\n  ]);\n\n  const handleFeatureEnter = useCallback(\n    (record: FeatureRecord) => {\n      setHoveredFeatureIndex(record.index);\n      setTooltipData({\n        featureIndex: record.index,\n        x: record.centroid?.x ?? width / 2,\n        y: record.centroid?.y ?? height / 2,\n        feature: record.feature,\n      });\n    },\n    [height, setHoveredFeatureIndex, setTooltipData, width]\n  );\n\n  const handleFeatureLeave = useCallback(() => {\n    setHoveredFeatureIndex(null);\n    setTooltipData(null);\n  }, [setHoveredFeatureIndex, setTooltipData]);\n\n  const layerProps = {\n    baseOpacity: 0.85,\n    dimOpacity: fadedOpacity,\n    hoveredIndex: hoveredFeatureIndex,\n    onFeatureEnter: handleFeatureEnter,\n    onFeatureLeave: handleFeatureLeave,\n    records,\n    stroke,\n    strokeWidth,\n  };\n\n  return (\n    <g className=\"choropleth-features\">\n      {patterns ? <defs>{patterns}</defs> : null}\n      {isLoaded ? (\n        <StaticFeatureLayer {...layerProps} />\n      ) : (\n        <EnterFeatureLayer {...layerProps} revealEpoch={revealEpoch} />\n      )}\n    </g>\n  );\n});\n\nChoroplethFeature.displayName = \"ChoroplethFeature\";\n\nexport default ChoroplethFeature;\n",
      "type": "registry:component",
      "target": "components/charts/choropleth/choropleth-feature.tsx"
    },
    {
      "path": "src/charts/choropleth/choropleth-graticule.tsx",
      "content": "\"use client\";\n\nimport { Graticule } from \"@visx/geo\";\nimport { memo } from \"react\";\nimport { useChoroplethStable } from \"./choropleth-context\";\n\nexport interface ChoroplethGraticuleProps {\n  /** Stroke color for graticule lines. Default: rgba(255,255,255,0.1) */\n  stroke?: string;\n  /** Stroke width for graticule lines. Default: 0.5 */\n  strokeWidth?: number;\n  /** Step intervals for graticule lines [longitude, latitude] in degrees. Default: [10, 10] */\n  step?: [number, number];\n}\n\nexport const ChoroplethGraticule = memo(function ChoroplethGraticule({\n  stroke = \"rgba(255,255,255,0.1)\",\n  strokeWidth = 0.5,\n  step,\n}: ChoroplethGraticuleProps) {\n  const { rawPathGenerator } = useChoroplethStable();\n\n  return (\n    <Graticule\n      graticule={(g) => rawPathGenerator(g) || \"\"}\n      step={step}\n      stroke={stroke}\n      strokeWidth={strokeWidth}\n    />\n  );\n});\n\nChoroplethGraticule.displayName = \"ChoroplethGraticule\";\n\nexport default ChoroplethGraticule;\n",
      "type": "registry:component",
      "target": "components/charts/choropleth/choropleth-graticule.tsx"
    },
    {
      "path": "src/charts/choropleth/choropleth-tooltip.tsx",
      "content": "\"use client\";\n\nimport { intFmt } from \"../chart-formatters\";\nimport { TooltipBox } from \"../tooltip/tooltip-box\";\nimport { TooltipContent, type TooltipRow } from \"../tooltip/tooltip-content\";\nimport {\n  type ChoroplethFeature,\n  useChoroplethInteraction,\n  useChoroplethStable,\n  useChoroplethZoom,\n} from \"./choropleth-context\";\n\nexport interface ChoroplethTooltipProps {\n  /** Custom content renderer for feature tooltips */\n  content?: (props: {\n    feature: ChoroplethFeature;\n    index: number;\n  }) => React.ReactNode;\n  /** Value formatter function */\n  formatValue?: (value: number) => string;\n  /** Get the display name for a feature. Default: uses feature.properties.name */\n  getFeatureName?: (feature: ChoroplethFeature, index: number) => string;\n  /** Get the value for a feature (for display in tooltip) */\n  getFeatureValue?: (\n    feature: ChoroplethFeature,\n    index: number\n  ) => number | undefined;\n  /** Label for the value row. Default: \"Value\" */\n  valueLabel?: string;\n  /** Custom class name */\n  className?: string;\n  /** Inline styles for the tooltip panel (background, blur, etc.). */\n  panelStyle?: React.CSSProperties;\n  /**\n   * Tooltip panel background color (CSS variable or color value).\n   * Default: `var(--chart-tooltip-background)`.\n   */\n  backgroundColor?: string;\n}\n\nexport function ChoroplethTooltip({\n  content,\n  formatValue = intFmt,\n  getFeatureName,\n  getFeatureValue,\n  valueLabel = \"Value\",\n  className = \"\",\n  panelStyle,\n  backgroundColor,\n}: ChoroplethTooltipProps) {\n  const { containerRef, width, height, features } = useChoroplethStable();\n  const { tooltipData } = useChoroplethInteraction();\n  const { zoom } = useChoroplethZoom();\n\n  if (!tooltipData) {\n    return null;\n  }\n\n  // Apply zoom transform to centroid position\n  let x = tooltipData.x;\n  let y = tooltipData.y;\n\n  if (zoom) {\n    // Apply the zoom transform matrix to the tooltip position\n    const transformed = zoom.applyToPoint({ x, y });\n    x = transformed.x;\n    y = transformed.y;\n  }\n\n  const feature = features[tooltipData.featureIndex];\n  if (!feature) {\n    return null;\n  }\n\n  // Get feature name\n  const featureName = getFeatureName\n    ? getFeatureName(feature, tooltipData.featureIndex)\n    : (feature.properties?.name ?? `Feature ${tooltipData.featureIndex}`);\n\n  // Custom content\n  if (content) {\n    return (\n      <TooltipBox\n        backgroundColor={backgroundColor}\n        className={className}\n        containerHeight={height}\n        containerRef={containerRef}\n        containerWidth={width}\n        panelStyle={panelStyle}\n        visible\n        x={x}\n        y={y}\n      >\n        {content({ feature, index: tooltipData.featureIndex })}\n      </TooltipBox>\n    );\n  }\n\n  // Default tooltip with optional value\n  const value = getFeatureValue?.(feature, tooltipData.featureIndex);\n  const rows: TooltipRow[] =\n    value === undefined\n      ? []\n      : [\n          {\n            color: \"var(--chart-1)\",\n            label: valueLabel,\n            value: formatValue(value),\n          },\n        ];\n\n  return (\n    <TooltipBox\n      backgroundColor={backgroundColor}\n      className={className}\n      containerHeight={height}\n      containerRef={containerRef}\n      containerWidth={width}\n      panelStyle={panelStyle}\n      visible\n      x={x}\n      y={y}\n    >\n      <TooltipContent rows={rows} title={featureName} />\n    </TooltipBox>\n  );\n}\n\nChoroplethTooltip.displayName = \"ChoroplethTooltip\";\n\nexport default ChoroplethTooltip;\n",
      "type": "registry:component",
      "target": "components/charts/choropleth/choropleth-tooltip.tsx"
    },
    {
      "path": "src/charts/choropleth/index.ts",
      "content": "export type { TransformMatrix } from \"@visx/zoom\";\nexport { ChoroplethChart, type ChoroplethChartProps } from \"./choropleth-chart\";\nexport {\n  type ChoroplethContextValue,\n  type ChoroplethFeature,\n  type ChoroplethFeatureProperties,\n  ChoroplethProvider,\n  type ChoroplethTooltipData,\n  choroplethCssVars,\n  defaultChoroplethColors,\n  type Margin,\n  useChoropleth,\n  useChoroplethZoom,\n} from \"./choropleth-context\";\nexport {\n  ChoroplethFeature as ChoroplethFeatureComponent,\n  type ChoroplethFeatureProps,\n} from \"./choropleth-feature\";\nexport {\n  ChoroplethGraticule,\n  type ChoroplethGraticuleProps,\n} from \"./choropleth-graticule\";\nexport {\n  ChoroplethTooltip,\n  type ChoroplethTooltipProps,\n} from \"./choropleth-tooltip\";\n",
      "type": "registry:component",
      "target": "components/charts/choropleth/index.ts"
    }
  ]
}