{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "hexbin-map",
  "title": "Hexbin Map",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/hexbin-map/HexbinMap.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface HexState {\n    id: string\n    name: string\n    col: number\n    row: number\n  }\n\n  export interface HexbinDatum {\n    value: number\n    // Widened to string (the Vue twin uses a status union): demos and\n    // consumers pass domain-specific statuses like 'optimal' / 'elevated'.\n    status?: string\n    description?: string\n    color?: string\n  }\n\n  export interface HexbinMapProps extends HTMLAttributes<HTMLDivElement> {\n    shape?: 'hexagon' | 'square'\n    preset?: 'us-states' | 'world-regions'\n    items?: HexState[]\n    data?: Record<string, HexbinDatum>\n    /** Bind with `bind:selected`. */\n    selected?: string\n    /** React-parity selection callback (fires alongside the `bind:selected` update). */\n    onSelectedChange?: (id: string | undefined) => void\n    showLabels?: boolean\n    showValues?: boolean\n    valueFormatter?: (value: number) => string\n    colorRamp?: string[]\n    emptyColor?: string\n    height?: number | string\n    interactive?: boolean\n    ariaLabel?: string\n    /** Fired when a region is selected. Named `onSelect` (capital S) because lowercase `onselect` collides with the DOM select-event handler in HTMLAttributes. */\n    onSelect?: (payload: { id: string; name: string; datum?: HexbinDatum }) => void\n  }\n\n  export const US_HEX_STATES: HexState[] = [\n    { id: 'AK', name: 'Alaska', col: 0, row: 0 },\n    { id: 'ME', name: 'Maine', col: 11, row: 0 },\n    { id: 'WA', name: 'Washington', col: 1, row: 1 },\n    { id: 'ID', name: 'Idaho', col: 2, row: 1 },\n    { id: 'MT', name: 'Montana', col: 3, row: 1 },\n    { id: 'ND', name: 'North Dakota', col: 4, row: 1 },\n    { id: 'MN', name: 'Minnesota', col: 5, row: 1 },\n    { id: 'IL', name: 'Illinois', col: 6, row: 1 },\n    { id: 'WI', name: 'Wisconsin', col: 7, row: 1 },\n    { id: 'MI', name: 'Michigan', col: 8, row: 1 },\n    { id: 'NY', name: 'New York', col: 9, row: 1 },\n    { id: 'VT', name: 'Vermont', col: 10, row: 1 },\n    { id: 'NH', name: 'New Hampshire', col: 11, row: 1 },\n    { id: 'OR', name: 'Oregon', col: 1, row: 2 },\n    { id: 'NV', name: 'Nevada', col: 2, row: 2 },\n    { id: 'WY', name: 'Wyoming', col: 3, row: 2 },\n    { id: 'SD', name: 'South Dakota', col: 4, row: 2 },\n    { id: 'IA', name: 'Iowa', col: 5, row: 2 },\n    { id: 'IN', name: 'Indiana', col: 6, row: 2 },\n    { id: 'OH', name: 'Ohio', col: 7, row: 2 },\n    { id: 'PA', name: 'Pennsylvania', col: 8, row: 2 },\n    { id: 'NJ', name: 'New Jersey', col: 9, row: 2 },\n    { id: 'MA', name: 'Massachusetts', col: 10, row: 2 },\n    { id: 'RI', name: 'Rhode Island', col: 11, row: 2 },\n    { id: 'CA', name: 'California', col: 1, row: 3 },\n    { id: 'UT', name: 'Utah', col: 2, row: 3 },\n    { id: 'CO', name: 'Colorado', col: 3, row: 3 },\n    { id: 'NE', name: 'Nebraska', col: 4, row: 3 },\n    { id: 'MO', name: 'Missouri', col: 5, row: 3 },\n    { id: 'KY', name: 'Kentucky', col: 6, row: 3 },\n    { id: 'WV', name: 'West Virginia', col: 7, row: 3 },\n    { id: 'VA', name: 'Virginia', col: 8, row: 3 },\n    { id: 'MD', name: 'Maryland', col: 9, row: 3 },\n    { id: 'DE', name: 'Delaware', col: 10, row: 3 },\n    { id: 'AZ', name: 'Arizona', col: 2, row: 4 },\n    { id: 'NM', name: 'New Mexico', col: 3, row: 4 },\n    { id: 'KS', name: 'Kansas', col: 4, row: 4 },\n    { id: 'AR', name: 'Arkansas', col: 5, row: 4 },\n    { id: 'TN', name: 'Tennessee', col: 6, row: 4 },\n    { id: 'NC', name: 'North Carolina', col: 7, row: 4 },\n    { id: 'SC', name: 'South Carolina', col: 8, row: 4 },\n    { id: 'DC', name: 'District of Columbia', col: 9, row: 4 },\n    { id: 'CT', name: 'Connecticut', col: 10, row: 4 },\n    { id: 'OK', name: 'Oklahoma', col: 4, row: 5 },\n    { id: 'LA', name: 'Louisiana', col: 5, row: 5 },\n    { id: 'MS', name: 'Mississippi', col: 6, row: 5 },\n    { id: 'AL', name: 'Alabama', col: 7, row: 5 },\n    { id: 'GA', name: 'Georgia', col: 8, row: 5 },\n    { id: 'HI', name: 'Hawaii', col: 0, row: 6 },\n    { id: 'TX', name: 'Texas', col: 4, row: 6 },\n    { id: 'FL', name: 'Florida', col: 8, row: 6 },\n  ]\n\n  export const WORLD_HEX_REGIONS: HexState[] = [\n    { id: 'CA', name: 'Canada', col: 2, row: 0 },\n    { id: 'GL', name: 'Greenland', col: 4, row: 0 },\n    { id: 'NO', name: 'Nordics', col: 6, row: 0 },\n    { id: 'US-W', name: 'US West', col: 1, row: 1 },\n    { id: 'US-E', name: 'US East', col: 2, row: 1 },\n    { id: 'UK', name: 'United Kingdom', col: 5, row: 1 },\n    { id: 'EU-W', name: 'Western Europe', col: 6, row: 1 },\n    { id: 'EU-E', name: 'Eastern Europe', col: 7, row: 1 },\n    { id: 'RU', name: 'Northern Eurasia', col: 8, row: 1 },\n    { id: 'MX', name: 'Mexico', col: 1, row: 2 },\n    { id: 'MED', name: 'Mediterranean', col: 6, row: 2 },\n    { id: 'ME', name: 'Middle East', col: 7, row: 2 },\n    { id: 'CN', name: 'East Asia', col: 8, row: 2 },\n    { id: 'JP', name: 'Japan', col: 9, row: 2 },\n    { id: 'BR', name: 'Brazil', col: 2, row: 3 },\n    { id: 'AF-N', name: 'North Africa', col: 5, row: 3 },\n    { id: 'IN', name: 'South Asia', col: 7, row: 3 },\n    { id: 'SEA', name: 'Southeast Asia', col: 8, row: 3 },\n    { id: 'AR', name: 'South Cone', col: 2, row: 4 },\n    { id: 'AF-S', name: 'Sub-Saharan', col: 5, row: 4 },\n    { id: 'AU', name: 'Australia & Oceania', col: 8, row: 4 },\n  ]\n\n  // Geographic coordinates mapping for placing Hexbins in space\n  const US_CENTROIDS: Record<string, [number, number]> = {\n    AK: [-152.4, 64.2],\n    ME: [-69.4, 45.3],\n    WA: [-120.7, 47.7],\n    ID: [-114.7, 44.1],\n    MT: [-110.4, 46.9],\n    ND: [-100.5, 47.5],\n    MN: [-94.6, 46.7],\n    IL: [-89.4, 40.6],\n    WI: [-89.6, 43.8],\n    MI: [-85.6, 44.3],\n    NY: [-74.2, 43.3],\n    VT: [-72.6, 44.6],\n    NH: [-71.6, 43.2],\n    OR: [-120.5, 43.8],\n    NV: [-116.4, 38.8],\n    WY: [-107.3, 43.1],\n    SD: [-99.9, 44.3],\n    IA: [-93.5, 42.0],\n    IN: [-86.1, 40.3],\n    OH: [-82.9, 40.4],\n    PA: [-77.2, 41.2],\n    NJ: [-74.4, 40.1],\n    MA: [-71.4, 42.4],\n    RI: [-71.5, 41.6],\n    CA: [-119.4, 36.8],\n    UT: [-111.1, 39.3],\n    CO: [-105.8, 39.5],\n    NE: [-99.9, 41.5],\n    MO: [-91.8, 37.9],\n    KY: [-84.3, 37.8],\n    WV: [-80.4, 38.6],\n    VA: [-78.7, 37.4],\n    MD: [-76.6, 39.0],\n    DE: [-75.5, 39.0],\n    AZ: [-111.1, 34.0],\n    NM: [-106.0, 34.5],\n    KS: [-98.5, 38.5],\n    AR: [-92.3, 34.8],\n    TN: [-86.6, 35.5],\n    NC: [-79.0, 35.8],\n    SC: [-81.2, 33.8],\n    DC: [-77.0, 38.9],\n    CT: [-72.7, 41.6],\n    OK: [-97.5, 35.0],\n    LA: [-91.9, 30.9],\n    MS: [-89.7, 32.4],\n    AL: [-86.9, 32.3],\n    GA: [-83.6, 32.2],\n    HI: [-157.8, 21.3],\n    TX: [-99.9, 31.9],\n    FL: [-81.5, 27.6],\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Globe } from '@lucide/svelte'\n  // @ts-ignore: 'map' ships from a sibling registry item (see hexbin-map.registry.ts); unavailable until all batches merge.\n  import { Map, MapMarker } from '$lib/components/ui/map'\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    shape = 'hexagon',\n    preset = 'us-states',\n    items,\n    data = {},\n    selected = $bindable<string | undefined>(undefined),\n    showLabels = true,\n    showValues = false,\n    valueFormatter = (v: number) => v.toLocaleString(),\n    colorRamp = [\n      'oklch(0.92 0.05 162)',\n      'oklch(0.82 0.10 162)',\n      'oklch(0.72 0.14 162)',\n      'oklch(0.62 0.17 162)',\n      'oklch(0.50 0.18 162)',\n      'oklch(0.38 0.16 162)',\n    ],\n    emptyColor = 'rgba(255, 255, 255, 0.08)',\n    height = 480,\n    interactive = true,\n    ariaLabel = 'Cartogram Tile Grid Map',\n    onSelectedChange,\n    onSelect,\n    ...restProps\n  }: HexbinMapProps = $props()\n\n  let currentProjection = $state<'globe' | 'mercator'>(preset === 'world-regions' ? 'globe' : 'mercator')\n\n  const activeItems = $derived(\n    items && items.length > 0 ? items : preset === 'world-regions' ? WORLD_HEX_REGIONS : US_HEX_STATES,\n  )\n\n  const values = $derived(\n    Object.values(data)\n      .map((d) => d.value)\n      .filter((v) => typeof v === 'number'),\n  )\n  const minValue = $derived(values.length ? Math.min(...values) : 0)\n  const maxValue = $derived(values.length ? Math.max(...values) : 100)\n\n  function getColor(datum?: HexbinDatum): string {\n    if (!datum) return emptyColor\n    if (datum.color) return datum.color\n\n    const val = datum.value\n    const span = maxValue - minValue\n    const normalized = span > 0 ? (val - minValue) / span : 0.5\n    const idx = Math.min(colorRamp.length - 1, Math.floor(normalized * colorRamp.length))\n    return colorRamp[idx] ?? emptyColor\n  }\n\n  const activeStateItem = $derived(activeItems.find((it) => it.id === selected))\n  const activeStateDatum = $derived(selected ? data[selected] : undefined)\n\n  function handleSelect(item: HexState) {\n    if (!interactive) return\n    selected = item.id\n    onSelectedChange?.(item.id)\n    onSelect?.({ id: item.id, name: item.name, datum: data[item.id] })\n  }\n\n  function clearSelection() {\n    selected = undefined\n    onSelectedChange?.(undefined)\n  }\n\n  function getCoords(item: HexState): [number, number] {\n    if (US_CENTROIDS[item.id]) return US_CENTROIDS[item.id]\n    const lng = -120 + item.col * 14\n    const lat = 50 - item.row * 8\n    return [lng, lat]\n  }\n\n  const mapCenter = $derived<[number, number]>(preset === 'world-regions' ? [0, 20] : [-97, 39])\n  const mapZoom = $derived(preset === 'world-regions' ? 1.5 : 3.6)\n\n  const heightStyle = $derived(\n    typeof height === 'number' ? `${height}px` : /^\\d+$/.test(String(height)) ? `${height}px` : height,\n  )\n\n  function toggleProjection() {\n    currentProjection = currentProjection === 'globe' ? 'mercator' : 'globe'\n  }\n</script>\n\n<div\n  data-uipkge\n  data-slot=\"hexbin-map\"\n  aria-label={ariaLabel}\n  {...restProps}\n  class={cn('border-border bg-card group relative w-full overflow-hidden rounded-xl border shadow-xs', className)}\n  style=\"height: {heightStyle}\"\n>\n  <Map variant=\"dark\" projection={currentProjection} center={mapCenter} zoom={mapZoom} class=\"size-full\">\n    {#each activeItems as item (item.id)}\n      <MapMarker\n        lngLat={getCoords(item)}\n        anchor=\"center\"\n        class={cn(\n          'cursor-pointer transition-transform select-none',\n          selected === item.id ? 'z-30 scale-115' : 'z-20 hover:scale-105',\n        )}\n      >\n        <button\n          type=\"button\"\n          class=\"group/hex relative flex size-10 items-center justify-center font-mono\"\n          onclick={() => handleSelect(item)}\n        >\n          <!-- Hexagon Shape Container -->\n          <div\n            class=\"absolute inset-0 flex items-center justify-center\"\n            style=\"clip-path: {shape === 'hexagon'\n              ? 'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'\n              : 'none'}; background-color: {getColor(\n              data[item.id],\n            )}; border: {selected === item.id\n              ? '2px solid white'\n              : '1px solid rgba(255,255,255,0.2)'}; box-shadow: {selected === item.id\n              ? `0 0 14px ${getColor(data[item.id])}`\n              : 'none'};\"\n          ></div>\n\n          <!-- Label & Metric -->\n          <div class=\"relative z-10 flex flex-col items-center justify-center text-center\">\n            {#if showLabels}\n              <span class=\"text-[10px] font-bold text-white drop-shadow-sm\">\n                {item.id}\n              </span>\n            {/if}\n            {#if showValues && data[item.id]}\n              <span class=\"text-[8px] font-semibold text-white/90\">\n                {Math.round(data[item.id].value)}\n              </span>\n            {/if}\n          </div>\n        </button>\n      </MapMarker>\n    {/each}\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      onclick={toggleProjection}\n    >\n      <Globe class=\"size-3.5\" />\n      <span class=\"capitalize\">{currentProjection}</span>\n    </button>\n  </div>\n\n  <!-- Active State Detail Card -->\n  {#if activeStateItem}\n    <div\n      class=\"border-border/80 bg-card/95 animate-in fade-in slide-in-from-bottom-2 absolute bottom-3 left-3 z-10 max-w-sm rounded-xl border p-3.5 shadow-lg backdrop-blur-md duration-150\"\n    >\n      <div class=\"flex items-start justify-between gap-3\">\n        <div>\n          <div class=\"flex items-center gap-2\">\n            <span class=\"size-2 rounded-full\" style=\"background-color: {getColor(activeStateDatum)}\"></span>\n            <span class=\"text-muted-foreground font-mono text-xs tracking-wider uppercase\">\n              {activeStateDatum?.status || 'Region'}\n            </span>\n          </div>\n          <h4 class=\"text-foreground mt-0.5 text-sm font-semibold\">\n            {activeStateItem.name} ({activeStateItem.id})\n          </h4>\n        </div>\n        <button type=\"button\" class=\"text-muted-foreground hover:text-foreground text-xs\" onclick={clearSelection}>\n          ✕\n        </button>\n      </div>\n\n      {#if activeStateDatum}\n        <div class=\"border-border/60 mt-2.5 flex items-baseline justify-between border-t pt-2 font-mono text-xs\">\n          <span class=\"text-muted-foreground\">Adoption Metric</span>\n          <span class=\"text-foreground font-semibold\"> {valueFormatter(activeStateDatum.value)}% </span>\n        </div>\n      {/if}\n\n      {#if activeStateDatum?.description}\n        <p class=\"text-muted-foreground mt-1.5 text-xs leading-relaxed\">\n          {activeStateDatum.description}\n        </p>\n      {/if}\n    </div>\n  {/if}\n\n  <!-- Bottom-Right Color Ramp Legend -->\n  {#if values.length > 0}\n    <div\n      class=\"border-border/70 bg-card/85 absolute right-3 bottom-3 z-10 hidden items-center gap-2 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-[10px]\">{minValue}</span>\n      <div class=\"flex h-2.5 gap-0.5 overflow-hidden rounded-xs\">\n        {#each colorRamp as c, i (i)}\n          <div class=\"w-3\" style=\"background-color: {c}\"></div>\n        {/each}\n      </div>\n      <span class=\"text-foreground font-mono text-[10px] font-semibold\">{maxValue}</span>\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/hexbin-map/HexbinMap.svelte"
    },
    {
      "path": "packages/registry-svelte/components/hexbin-map/index.ts",
      "content": "export {\n  default as HexbinMap,\n  default,\n  US_HEX_STATES,\n  WORLD_HEX_REGIONS,\n  type HexbinMapProps,\n  type HexbinDatum,\n  type HexState,\n} from './HexbinMap.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/hexbin-map/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/map.json"
  ],
  "description": "US 50 states + DC hexbin cartogram chart rendered as pure dependency-free SVG. Pointy-top hexagonal grid with automatic color scale ramps, state abbreviation labels, metric values, interactive hover tooltips, and keyboard-accessible state selection.",
  "categories": [
    "chart"
  ]
}