{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "choropleth-map-chart",
  "title": "Choropleth Map Chart",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/charts/choropleth-map-chart/ChoroplethMapChart.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Map, MapMarker, MapSource, MapLayer, type MapVariant } from '@/components/ui/map'\nimport { cn } from '@/lib/utils'\nimport { Globe } from 'lucide-vue-next'\n\nexport interface ChoroplethDatum {\n  id: string\n  value: number\n  name?: string\n}\n\nexport interface ChoroplethPin {\n  name: string\n  coord: [number, number]\n  color?: string\n}\n\nexport interface ChoroplethLink {\n  from: [number, number]\n  to: [number, number]\n  label?: string\n}\n\ninterface Props {\n  geoJson: any\n  mapName?: string\n  data?: ChoroplethDatum[]\n  idField?: 'id' | 'name'\n  pins?: ChoroplethPin[]\n  links?: ChoroplethLink[]\n  showScale?: boolean\n  height?: number | string\n  center?: [number, number]\n  zoom?: number\n  variant?: MapVariant\n  projection?: 'globe' | 'mercator'\n  class?: string\n  ariaLabel?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  mapName: 'uipkge-map',\n  idField: 'id',\n  showScale: true,\n  height: 420,\n  center: () => [0, 20],\n  zoom: 1.5,\n  variant: 'dark',\n  projection: 'globe',\n})\n\nconst currentProjection = ref<'globe' | 'mercator'>(props.projection)\nconst hoveredRegion = ref<{ title: string; value: number | null } | null>(null)\n\nfunction toggleProjection() {\n  currentProjection.value = currentProjection.value === 'globe' ? 'mercator' : 'globe'\n}\n\nconst dataMap = computed(() => {\n  const map = new globalThis.Map<string, number>()\n  if (!props.data) return map\n  for (const d of props.data) {\n    map.set(String(d.id).toLowerCase(), d.value)\n    if (d.name) map.set(String(d.name).toLowerCase(), d.value)\n  }\n  return map\n})\n\nconst values = computed(() => (props.data ? props.data.map((d) => d.value) : []))\nconst minValue = computed(() => (values.value.length ? Math.min(...values.value) : 0))\nconst maxValue = computed(() => (values.value.length ? Math.max(...values.value) : 100))\n\nconst enrichedGeoJson = computed(() => {\n  if (!props.geoJson || !props.geoJson.features) return props.geoJson\n  const features = props.geoJson.features.map((f: any) => {\n    const idVal = f.id !== undefined ? String(f.id).toLowerCase() : ''\n    const nameVal = f.properties?.name ? String(f.properties.name).toLowerCase() : ''\n    const matchedVal = dataMap.value.get(idVal) ?? dataMap.value.get(nameVal) ?? null\n\n    return {\n      ...f,\n      properties: {\n        ...f.properties,\n        value: matchedVal,\n        title: f.properties?.name || f.id || 'Region',\n      },\n    }\n  })\n\n  return {\n    ...props.geoJson,\n    features,\n  }\n})\n\nconst fillPaint = computed(() => ({\n  'fill-color': [\n    'case',\n    ['!=', ['get', 'value'], null],\n    [\n      'interpolate',\n      ['linear'],\n      ['get', 'value'],\n      minValue.value,\n      'rgba(56, 189, 248, 0.25)',\n      maxValue.value,\n      'rgba(56, 189, 248, 0.9)',\n    ],\n    'rgba(255, 255, 255, 0.04)',\n  ],\n  'fill-opacity': 0.85,\n}))\n\nconst strokePaint = {\n  'line-color': 'rgba(255, 255, 255, 0.25)',\n  'line-width': 1,\n}\n\nconst linksGeoJson = computed(() => {\n  if (!props.links || !props.links.length) return null\n  return {\n    type: 'FeatureCollection',\n    features: props.links.map((link) => ({\n      type: 'Feature',\n      properties: { label: link.label },\n      geometry: {\n        type: 'LineString',\n        coordinates: [link.from, link.to],\n      },\n    })),\n  }\n})\n\nconst linkLinePaint = {\n  'line-color': 'rgba(245, 158, 11, 0.75)',\n  'line-width': 2,\n  'line-dasharray': [2, 2],\n}\n</script>\n\n<template>\n  <div\n    :class=\"cn('border-border bg-card group relative w-full overflow-hidden rounded-xl border shadow-xs', props.class)\"\n    :style=\"{\n      height:\n        typeof props.height === 'number'\n          ? `${props.height}px`\n          : /^\\d+$/.test(String(props.height))\n            ? `${props.height}px`\n            : props.height,\n    }\"\n  >\n    <Map :variant=\"variant\" :projection=\"currentProjection\" :center=\"center\" :zoom=\"zoom\" class=\"size-full\">\n      <!-- GeoJSON Choropleth Source & Layers -->\n      <MapSource id=\"choropleth-source\" type=\"geojson\" :data=\"enrichedGeoJson\">\n        <MapLayer id=\"choropleth-fill\" type=\"fill\" :paint=\"fillPaint\" />\n        <MapLayer id=\"choropleth-stroke\" type=\"line\" :paint=\"strokePaint\" />\n      </MapSource>\n\n      <!-- Links Layer -->\n      <MapSource v-if=\"linksGeoJson\" id=\"choropleth-links-source\" type=\"geojson\" :data=\"linksGeoJson\">\n        <MapLayer id=\"choropleth-links\" type=\"line\" :paint=\"linkLinePaint\" />\n      </MapSource>\n\n      <!-- Point Pins -->\n      <MapMarker v-for=\"(pin, i) in pins || []\" :key=\"i\" :lng-lat=\"pin.coord\" anchor=\"bottom\">\n        <div class=\"flex flex-col items-center\">\n          <span\n            class=\"size-3 rounded-full shadow-md ring-4\"\n            :style=\"{\n              backgroundColor: pin.color || 'oklch(0.65 0.20 145)',\n              boxShadow: `0 0 10px ${pin.color || 'oklch(0.65 0.20 145)'}`,\n            }\"\n          />\n          <span\n            class=\"border-border/80 bg-background/90 mt-1 rounded border px-1.5 py-0.5 font-mono text-[10px] font-semibold shadow-xs backdrop-blur-xs\"\n          >\n            {{ pin.name }}\n          </span>\n        </div>\n      </MapMarker>\n    </Map>\n\n    <!-- Top-Right Projection Switcher -->\n    <div\n      class=\"border-border/70 bg-card/85 absolute top-3 right-3 z-10 flex items-center gap-1 rounded-lg border p-1 shadow-xs backdrop-blur-md\"\n    >\n      <button\n        type=\"button\"\n        class=\"text-muted-foreground hover:bg-muted hover:text-foreground flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition\"\n        @click=\"toggleProjection\"\n      >\n        <Globe class=\"size-3.5\" />\n        <span class=\"capitalize\">{{ currentProjection }}</span>\n      </button>\n    </div>\n\n    <!-- Bottom-Right Continuous Value Scale Legend -->\n    <div\n      v-if=\"showScale && values.length > 0\"\n      class=\"border-border/70 bg-card/85 absolute right-3 bottom-3 z-10 hidden items-center gap-2.5 rounded-lg border px-3 py-2 text-xs shadow-xs backdrop-blur-md sm:flex\"\n    >\n      <span class=\"text-muted-foreground font-mono text-[11px]\">Range</span>\n      <span class=\"text-muted-foreground font-mono text-[10px]\">{{ minValue }}</span>\n      <div class=\"h-2 w-24 rounded-full bg-gradient-to-r from-sky-400/30 to-sky-400\" />\n      <span class=\"text-foreground font-mono text-[10px] font-semibold\">{{ maxValue }}</span>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/charts/choropleth-map-chart/ChoroplethMapChart.vue"
    },
    {
      "path": "packages/registry-vue/components/charts/choropleth-map-chart/index.ts",
      "content": "export { default as ChoroplethMapChart } from './ChoroplethMapChart.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/charts/choropleth-map-chart/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/map.json"
  ],
  "description": "Choropleth map around Apache ECharts with bring-your-own GeoJSON (registered via registerMap). Value-shaded areas with roam, plus pin markers and curved links. Theme-aware via registry tokens.",
  "categories": [
    "chart"
  ]
}