{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "hexbin-map",
  "title": "Hexbin Map",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/charts/hexbin-map/hexbin-map.component.ts",
      "content": "import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { UiMapComponent, UiMapMarkerComponent } from '@/ui/map/map.component'\n\nexport interface HexState {\n  id: string\n  name: string\n  col: number\n  row: number\n}\n\nexport interface HexbinDatum {\n  value: number\n  /** Display-only status label (rendered uppercase). React/Vue declare a narrow union, but their\n   * own demos use domain statuses ('optimal', 'normal', ...), so this port accepts any string. */\n  status?: string\n  description?: string\n  color?: string\n}\n\nexport type HexbinShape = 'hexagon' | 'square'\nexport type HexbinPreset = 'us-states' | 'world-regions'\n\nexport 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\nexport 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\nconst 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\nfunction defaultValueFormatter(v: number): string {\n  return v.toLocaleString()\n}\n\n/**\n * Angular port of the UIPKGE HexbinMap (React/Vue parity): an interactive Mapbox hex\n * cartogram. Value-shaded hex markers at geographic centroids, click-to-select with a\n * detail card, a globe/mercator projection switcher, and a color-ramp legend.\n * `accessToken` is passed through to the inner map (React/Vue read it from env; Angular\n * needs it as an input).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-hexbin-map, [ui-hexbin-map]',\n  standalone: true,\n  imports: [UiMapComponent, UiMapMarkerComponent],\n  host: {\n    '[attr.data-slot]': '\"hexbin-map\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    <div [class]=\"bodyClass\" [style.height]=\"heightStyle\" [attr.aria-label]=\"ariaLabel\">\n      <ui-map\n        variant=\"dark\"\n        [projection]=\"currentProjection\"\n        [center]=\"mapCenter\"\n        [zoom]=\"mapZoom\"\n        [accessToken]=\"accessToken\"\n        class=\"size-full\"\n      >\n        @for (item of activeItems; track item.id) {\n          <ui-map-marker [lngLat]=\"getCoords(item)\" anchor=\"center\">\n            <button type=\"button\" [class]=\"markerClass(item)\" (click)=\"handleSelect(item)\">\n              <div\n                class=\"absolute inset-0 flex items-center justify-center\"\n                [style.clip-path]=\"hexClipPath\"\n                [style.background-color]=\"getColor(data[item.id])\"\n                [style.border]=\"markerBorder(item)\"\n                [style.box-shadow]=\"markerShadow(item)\"\n              ></div>\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\">{{ item.id }}</span>\n                }\n                @if (showValues && data[item.id]) {\n                  <span class=\"text-[8px] font-semibold text-white/90\">{{ roundedValue(item) }}</span>\n                }\n              </div>\n            </button>\n          </ui-map-marker>\n        }\n      </ui-map>\n\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          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            width=\"24\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            stroke-width=\"2\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            class=\"lucide lucide-globe size-3.5\"\n            aria-hidden=\"true\"\n          >\n            <circle cx=\"12\" cy=\"12\" r=\"10\" />\n            <path d=\"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20\" />\n            <path d=\"M2 12h20\" />\n          </svg>\n          <span class=\"capitalize\">{{ currentProjection }}</span>\n        </button>\n      </div>\n\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\n              type=\"button\"\n              aria-label=\"Clear selection\"\n              class=\"text-muted-foreground hover:text-foreground text-xs\"\n              (click)=\"clearSelection()\"\n            >\n              ✕\n            </button>\n          </div>\n\n          @if (activeStateDatum) {\n            <div\n              class=\"border-border/60 mt-2.5 flex items-baseline justify-between border-t pt-2 font-mono text-xs\"\n            >\n              <span class=\"text-muted-foreground\">Adoption Metric</span>\n              <span class=\"text-foreground font-semibold\">{{ valueFormatter(activeStateDatum.value) }}%</span>\n            </div>\n          }\n\n          @if (activeStateDatum?.description) {\n            <p class=\"text-muted-foreground mt-1.5 text-xs leading-relaxed\">\n              {{ activeStateDatum?.description }}\n            </p>\n          }\n        </div>\n      }\n\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            @for (c of colorRamp; track $index) {\n              <div class=\"w-3\" [style.background-color]=\"c\"></div>\n            }\n          </div>\n          <span class=\"text-foreground font-mono text-[10px] font-semibold\">{{ maxValue }}</span>\n        </div>\n      }\n    </div>\n  `,\n})\nexport class UiHexbinMapComponent {\n  @Input() shape: HexbinShape = 'hexagon'\n  @Input() preset: HexbinPreset = 'us-states'\n  @Input() items: HexState[] = []\n  @Input() data: Record<string, HexbinDatum> = {}\n  @Input() selected?: string\n  @Input() showLabels = true\n  @Input() showValues = false\n  @Input() valueFormatter: (value: number) => string = defaultValueFormatter\n  @Input() colorRamp: string[] = [\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  @Input() emptyColor = 'rgba(255, 255, 255, 0.08)'\n  @Input() height: number | string = 480\n  @Input() interactive = true\n  /** Mapbox token, passed through to the inner map (React/Vue read it from env). */\n  @Input() accessToken = ''\n  @Input() ariaLabel = 'Cartogram Tile Grid Map'\n  @Input('class') className?: string\n  @Output() selectedChange = new EventEmitter<string | undefined>()\n  @Output() select = new EventEmitter<{ id: string; name: string; datum?: HexbinDatum }>()\n\n  private internalSelected?: string\n  private projectionOverride?: 'globe' | 'mercator'\n\n  get hostClass(): string {\n    return cn('block w-full', this.className)\n  }\n\n  get bodyClass(): string {\n    return cn('border-border bg-card group relative w-full overflow-hidden rounded-xl border shadow-xs')\n  }\n\n  get heightStyle(): string {\n    if (typeof this.height === 'number') return `${this.height}px`\n    return /^\\d+$/.test(String(this.height)) ? `${this.height}px` : String(this.height)\n  }\n\n  get activeItems(): HexState[] {\n    if (this.items && this.items.length > 0) return this.items\n    return this.preset === 'world-regions' ? WORLD_HEX_REGIONS : US_HEX_STATES\n  }\n\n  get values(): number[] {\n    return Object.values(this.data)\n      .map((d) => d.value)\n      .filter((v) => typeof v === 'number')\n  }\n\n  get minValue(): number {\n    return this.values.length ? Math.min(...this.values) : 0\n  }\n\n  get maxValue(): number {\n    return this.values.length ? Math.max(...this.values) : 100\n  }\n\n  get activeSelected(): string | undefined {\n    return this.selected ?? this.internalSelected\n  }\n\n  get activeStateItem(): HexState | undefined {\n    return this.activeItems.find((it) => it.id === this.activeSelected)\n  }\n\n  get activeStateDatum(): HexbinDatum | undefined {\n    return this.activeSelected ? this.data[this.activeSelected] : undefined\n  }\n\n  get mapCenter(): [number, number] {\n    return this.preset === 'world-regions' ? [0, 20] : [-97, 39]\n  }\n\n  get mapZoom(): number {\n    return this.preset === 'world-regions' ? 1.5 : 3.6\n  }\n\n  get currentProjection(): 'globe' | 'mercator' {\n    return this.projectionOverride ?? (this.preset === 'world-regions' ? 'globe' : 'mercator')\n  }\n\n  get hexClipPath(): string {\n    return this.shape === 'hexagon' ? 'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)' : 'none'\n  }\n\n  getColor(datum?: HexbinDatum): string {\n    if (!datum) return this.emptyColor\n    if (datum.color) return datum.color\n    const span = this.maxValue - this.minValue\n    const normalized = span > 0 ? (datum.value - this.minValue) / span : 0.5\n    const idx = Math.min(this.colorRamp.length - 1, Math.floor(normalized * this.colorRamp.length))\n    return this.colorRamp[idx]\n  }\n\n  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  markerClass(item: HexState): string {\n    // Merges React's MapMarker className + button className: the Angular marker takes no class.\n    return cn(\n      'group/hex relative flex size-10 cursor-pointer items-center justify-center font-mono transition-transform select-none',\n      this.activeSelected === item.id ? 'z-30 scale-115' : 'z-20 hover:scale-105',\n    )\n  }\n\n  markerBorder(item: HexState): string {\n    return this.activeSelected === item.id ? '2px solid white' : '1px solid rgba(255,255,255,0.2)'\n  }\n\n  markerShadow(item: HexState): string {\n    return this.activeSelected === item.id ? `0 0 14px ${this.getColor(this.data[item.id])}` : 'none'\n  }\n\n  roundedValue(item: HexState): number {\n    return Math.round(this.data[item.id]?.value ?? 0)\n  }\n\n  handleSelect(item: HexState): void {\n    if (!this.interactive) return\n    this.internalSelected = item.id\n    this.selectedChange.emit(item.id)\n    this.select.emit({ id: item.id, name: item.name, datum: this.data[item.id] })\n  }\n\n  clearSelection(): void {\n    this.internalSelected = undefined\n    this.selectedChange.emit(undefined)\n  }\n\n  toggleProjection(): void {\n    this.projectionOverride = this.currentProjection === 'globe' ? 'mercator' : 'globe'\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/charts/hexbin-map/hexbin-map.component.ts"
    },
    {
      "path": "packages/registry-angular/components/charts/hexbin-map/index.ts",
      "content": "export {\n  UiHexbinMapComponent,\n  US_HEX_STATES,\n  WORLD_HEX_REGIONS,\n  type HexState,\n  type HexbinDatum,\n  type HexbinShape,\n  type HexbinPreset,\n} from './hexbin-map.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/charts/hexbin-map/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/map.json"
  ],
  "description": "Interactive Mapbox hex cartogram. US-states and world-regions presets with value-shaded hex markers, click-to-select detail cards, a projection switcher, and a color-ramp legend. Theme-aware via registry tokens.",
  "categories": [
    "chart"
  ]
}