{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dotted-map",
  "title": "Dotted Map",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A world map built from dots, with pulsing markers, optional labels and animated routes between places, as one lightweight SVG with a text summary for screen readers.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/motion-presets.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/dotted-map.tsx",
      "content": "// Ballmac UI: Dotted Map. https://ui.ballmac.com/components/dotted-map\n// Based on cobe (MIT, Copyright (c) 2021 Shu Ding): the land mask is derived from cobe's world texture; the rendering is new.\n\"use client\"\n\nimport * as React from \"react\"\nimport { motion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useReducedMotionSafe } from \"@/lib/ballmac/motion\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype MapTone = \"foreground\" | \"primary\" | \"chart-1\" | \"chart-2\" | \"chart-3\" | \"chart-4\" | \"chart-5\" | \"destructive\"\n\nconst COLOR: Record<MapTone, string> = {\n  foreground: \"var(--foreground)\",\n  primary: \"var(--primary)\",\n  \"chart-1\": \"var(--chart-1)\",\n  \"chart-2\": \"var(--chart-2)\",\n  \"chart-3\": \"var(--chart-3)\",\n  \"chart-4\": \"var(--chart-4)\",\n  \"chart-5\": \"var(--chart-5)\",\n  destructive: \"var(--destructive)\",\n}\n\ntype MapMarker = {\n  /** Latitude in degrees, north positive. */\n  lat: number\n  /** Longitude in degrees, east positive. */\n  lng: number\n  /** Name of the place. Always read by screen readers, and drawn on the map when `labels` is on. */\n  label?: string\n  /** Marker color. */\n  tone?: MapTone\n}\n\ntype MapArc = {\n  /** Start as [lat, lng]. */\n  from: [number, number]\n  /** End as [lat, lng]. */\n  to: [number, number]\n  /** Arc color. */\n  tone?: MapTone\n}\n\n// A 256 x 128 equirectangular land mask, one bit per cell, rows from 90 degrees north to 90 south.\nconst LAND =\n  \"////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////Dj///////6n////////////////////////////////8AAACAAAAgP8AP////////////////////////////8AAAP/gAAAAAAA+gf//////////////////////////wAAAfQAAAAAAAADg////////////////////g//////AAAAQAAAAGYAAAf8H//////////////////4AJ////8AAAAAAAAD4AAP//4AAfn/////////x/////4AB////wAAAAAAAAcAAL///gKAef////////+H/////8zD///+AAAAAAAAHAHH/////8A/AAO//P/+AAM//////+H///wAAAAAIAAcA//////////uAA/gP//z/5//////4f//+AAAAB/8AAB73//////////9/+D////////////w///gAAAAf//mf//f////////////+H////////////D//gAAAAD///f////////////////5////////////4P/4A/AAA/3+P////////////////CAf///////+8T/Af8AD8AAH+f/////////////////4AP////////ghTwA/gAAAAB/n//////////////////wA////////8APwAB8AAAAAP+f///////////////z/gAB/sX/////gA/mAAwAAAAA/4///////////////8OAAABeAD/////AD/8AAAAAAMBPB/////////////+ABwAAABkAH/////gH/wAAAAAAwDcf/////////////gAfAAAAQAAH/////wf/gAAAAADgPB/////////////4AB4AAAAAAAH/////3//wAAAAB3A///////////////8AHAAAAAAAAP/////P//gAAAAHeP///////////////8AYAAAAAAAAf///////8AAAAAB7////////////////wBAAAAAAAAA///////+YAAAAABf///////////////9gAAAAAAAAAB////v//DwAAAAAf/////5//////////0AAAAAAAAAAD///+P/8HAAAAAA/////+B/////////+QAAAAAAAAAAP////v/+AAAAAAB///9nwHf////////wAAAAAAAAAAB////2/9gAAAAAAH/v/ifj9////////+OAAAAAAAAAAH//////AAAAAAAP8nP8AfH/////////w4AAAAAAAAAAf/////8AAAAAAB/wnP3x+P////////4AAAAAAAAAAAA//////AAAAAAAH+CG7//4/////////AYAAAAAAAAAAD/////4AAAAAAAfwARn//j///////wYBgAAAAAAAAAAH/////AAAAAAAA+AmGf//P///////4wMAAAAAAAAAAAP////8AAAAAAAAj/AAF//////////DHwAAAAAAAAAAA/////gAAAAAAAH/8AAH/////////4J8AAAAAAAAAAAA////8AAAAAAAA//8GAf/////////wOAAAAAAAAAAAAB////AAAAAAAAH//8/L//////////gQAAAAAAAAAAAAG//+8AAAAAAAAf//////////////+AAAAAAAAAAAAAAP/8AYAAAAAAAD//////+////////4AAAAAAAAAAAAAAX/gBgAAAAAAA/////9/5////////AAAAAAAAAAAAAAAv+ACAAAAAAAD/////7/wf//////4AAAAAAAAAAAAAADf4AAAAAAAAAf/////v/uB//////IAAAAAAAAAAAAAAE/gBgAAAAAAD//////f/8B/////5gAAAAAAAAAAAAAAB+ADwAAAAAAP/////8//wH//f/+AAAAAAAAAAAAAAAAH4MBgAAAAAA//////7//AH/w/8gAAAAAAAAAAAAAAAAfxwA4AAAAAD//////n/4AP+B/mAAAAAAAAAAAAAAAAAf+ABAAAAAAP//////P/AA/gH/AGAAAAAAAAAAAAAAAAP4AAAAAAAA//////8/wAD8AX+AYAAAAAAAAAAAAAAAAD+AAAAAAAD//////78AAPgAf8BgAAAAAAAAAAAAAAAAD4AAAAAAAP///////AAAeAA/wDAAAAAAAAAAAAAAAAADgAAAAAAA///////gAAB4ACfADAAAAAAAAAAAAAAAAAGD+wAAAAB///////+AADgAI4AQAAAAAAAAAAAAAAAAAP//gAAAAD///////wAANAAhABQAAAAAAAAAAAAAAAAAb//AAAAAH///////AAAMADAAHAAAAAAAAAAAAAAAAAAH//AAAAAP//////4AAAwAGAMMAAAAAAAAAAAAAAAAAAf//gAAAAOw/////gAAAADcB4AAAAAAAAAAAAAAAAAAB///AAAAAAAf///8AAAAAOwPAAAAAAAAAAAAAAAAAAAP//8AAAAAAB////gAAAAAfB8AAAAAAAAAAAAAAAAAAB///4AAAAAAP///4AAAAAA8fzoAAAAAAAAAAAAAAAAAP///4AAAAAA//+/AAAAAABx/MGAAAAAAAAAAAAAAAAA////4AAAAAD//78AAAAAADz7gNgAAAAAAAAAAAAAAAB////+AAAAAH///gAAAAAAPHuO/4AAAAAAAAAAAAAAAP////8AAAAAP//8AAAAAAAYAcA/wAAAAAAAAAAAAAAAf////4AAAAAf//wAAAAAAAcAAA/sAAAAAAAAAAAAAAB/////gAAAAB///AAAAAAAA+AAD+AAAAAAAAAAAAAAAD////+AAAAAH//8AAAAAAAAADADMAAAAAAAAAAAAAAAP////wAAAAAP//4AAAAAAAAAAAAYAAAAAAAAAAAAAAAf///+AAAAAA///gAAAAAAAAAA4QAAAAAAAAAAAAAAAA////4AAAAAH//+DAAAAAAAAAPjAAAAAAAAAAAAAAAAD////gAAAAA///4cAAAAAAAAH+GAAAAAAAAAAAAAAAAH///8AAAAAD///jwAAAAAAAA/88AAAAAAAAAAAAAAAAH///wAAAAAP//4fAAAAAAAAH//wAAAgAAAAAAAAAAAAP///AAAAAAf//B4AAAAAAAA///gAAAAAAAAAAAAAAAA///4AAAAAB//4HgAAAAAAAf///AAAAAAAAAAAAAAAAD///gAAAAAD//gcAAAAAAAD///+AEAAAAAAAAAAAAAAP//8AAAAAAP//BwAAAAAAAf///8AAAAAAAAAAAAAAAA//+AAAAAAA//4HAAAAAAAB////wAAAAAAAAAAAAAAAH//wAAAAAAD//AAAAAAAAAH////gAAAAAAAAAAAAAAAf//AAAAAAAH/4AAAAAAAAAf///+AAAAAAAAAAAAAAAB//4AAAAAAAf/gAAAAAAAAB////4AAAAAAAAAAAAAAAH//AAAAAAAA/8AAAAAAAAAD////gAAAAAAAAAAAAAAAf/4AAAAAAAB/gAAAAAAAAAP///+AAAAAAAAAAAAAAAB//gAAAAAAAH8AAAAAAAAAA/gf/wAAAAAAAAAAAAAAAP/8AAAAAAAAdAAAAAAAAAADgAv/AAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAf4AAQAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAB/gAAgAAAAAAAAAAAAP/AAAAAAAAAAAAAAAAAAAAAAAB4AADgAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAH+AAAAAAAAAAAAAAAAAAAAAAAAHAADAAAAAAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAYAAcAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAg/gGB8AAAAAAAAAAAAAAAAAAfgAAAAAAAAAAAAfwAD///////8AAAAAAAAAAAAAAAAB/AAAAAAAAAAAA///8f////////4AAAAAAAAAAAAAAD/4AAAAAAAAPAf///////////////AAAAAAAAAAAAAAP/gAAAAA/////////////////////8AAAAAAAAAD/4D//AAAAA//////////////////////8AAAAABv/3P////4AAAAP//////////////////////wAAAA//////////4AAAH//////////////////////wAAAB///////////+AAH//////////////////////+AMA//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////w==\"\n\nconst MASK_W = 256\nconst MASK_H = 128\nlet bits: Uint8Array | null = null\n\nfunction isLand(lat: number, lng: number) {\n  if (!bits) {\n    const raw = atob(LAND)\n    bits = new Uint8Array(raw.length)\n    for (let i = 0; i < raw.length; i++) bits[i] = raw.charCodeAt(i)\n  }\n  const x = Math.min(MASK_W - 1, Math.max(0, Math.floor(((lng + 180) / 360) * MASK_W)))\n  const y = Math.min(MASK_H - 1, Math.max(0, Math.floor(((90 - lat) / 180) * MASK_H)))\n  const index = y * MASK_W + x\n  return ((bits[index >> 3]! >> (7 - (index & 7))) & 1) === 1\n}\n\ntype DottedMapProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Places to mark. */\n  markers?: MapMarker[]\n  /** Routes to draw between places, animated as if traffic is flowing. */\n  arcs?: MapArc[]\n  /** Number of dots across the whole width. More dots give a finer, denser map. */\n  dots?: number\n  /** Southernmost and northernmost latitude to show. */\n  latRange?: [number, number]\n  /** Color of the land dots. */\n  tone?: MapTone\n  /** Write each marker's label on the map. */\n  labels?: boolean\n  /** Accessible summary of the map. */\n  label?: string\n}\n\nconst WIDTH = 1000\n\nfunction project(lat: number, lng: number, top: number, span: number, height: number) {\n  return { x: ((lng + 180) / 360) * WIDTH, y: ((top - lat) / span) * height }\n}\n\n/** A world map made of dots, with markers and flowing routes. Pure SVG. */\nfunction DottedMap({ markers = [], arcs = [], dots = 150, latRange = [-56, 73], tone = \"foreground\", labels = false, label, className, ...props }: DottedMapProps) {\n  const msg = useMessages()\n  label ??= msg(\"dotted-map.label\", \"World map\")\n  const reduce = useReducedMotionSafe()\n  const [south, north] = latRange\n  const span = north - south\n  const height = (WIDTH * span) / 360\n  const spacing = WIDTH / dots\n\n  const land = React.useMemo(() => {\n    const rows = Math.floor(height / spacing)\n    let d = \"\"\n    for (let r = 0; r < rows; r++) {\n      const y = (r + 0.5) * spacing\n      const lat = north - (y / height) * span\n      const shift = r % 2 ? spacing / 2 : 0\n      for (let c = 0; c < dots; c++) {\n        const x = c * spacing + spacing / 2 + shift\n        if (x > WIDTH) continue\n        const lng = (x / WIDTH) * 360 - 180\n        if (isLand(lat, lng)) d += `M${x.toFixed(1)} ${y.toFixed(1)}h0`\n      }\n    }\n    return d\n  }, [dots, height, spacing, north, span])\n\n  const places = markers.map((m) => ({ ...m, ...project(m.lat, m.lng, north, span, height) }))\n  const routes = arcs.map((a) => {\n    const p = project(a.from[0], a.from[1], north, span, height)\n    const q = project(a.to[0], a.to[1], north, span, height)\n    const lift = Math.hypot(q.x - p.x, q.y - p.y) * 0.32\n    return { ...a, d: `M${p.x.toFixed(1)} ${p.y.toFixed(1)} Q${((p.x + q.x) / 2).toFixed(1)} ${(((p.y + q.y) / 2) - lift).toFixed(1)} ${q.x.toFixed(1)} ${q.y.toFixed(1)}` }\n  })\n  const summary = markers.filter((m) => m.label).map((m) => m.label)\n\n  return (\n    <div data-slot=\"dotted-map\" className={cn(\"relative w-full\", className)} {...props}>\n      <svg role=\"img\" aria-label={summary.length ? `${label}: ${summary.join(\", \")}` : label} viewBox={`0 0 ${WIDTH} ${height.toFixed(1)}`} className=\"block h-auto w-full overflow-visible\">\n        <path d={land} fill=\"none\" stroke={COLOR[tone]} strokeOpacity={tone === \"foreground\" ? 0.28 : 0.55} strokeWidth={spacing * 0.58} strokeLinecap=\"round\" />\n        {routes.map((r, i) => (\n          <g key={i} style={{ color: COLOR[r.tone ?? \"chart-1\"] }}>\n            <path d={r.d} fill=\"none\" stroke=\"currentColor\" strokeOpacity={0.22} strokeWidth=\"1.5\" strokeDasharray=\"2 6\" strokeLinecap=\"round\" />\n            <motion.path\n              d={r.d}\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2.2\"\n              strokeLinecap=\"round\"\n              initial={reduce ? { pathLength: 1, opacity: 0.9 } : { pathLength: 0, opacity: 0 }}\n              animate={reduce ? undefined : { pathLength: [0, 1, 1], opacity: [0, 1, 0] }}\n              transition={{ duration: 3.2, repeat: Infinity, ease: \"easeInOut\", delay: i * 0.7, times: [0, 0.6, 1] }}\n            />\n          </g>\n        ))}\n        {places.map((p, i) => (\n          <g key={i} style={{ color: COLOR[p.tone ?? \"chart-1\"] }}>\n            {!reduce && (\n              <motion.circle\n                cx={p.x}\n                cy={p.y}\n                fill=\"currentColor\"\n                initial={{ r: spacing * 0.9, opacity: 0.5 }}\n                animate={{ r: [spacing * 0.9, spacing * 3.4], opacity: [0.5, 0] }}\n                transition={{ duration: 2.4, repeat: Infinity, ease: \"easeOut\", delay: i * 0.35 }}\n              />\n            )}\n            <circle cx={p.x} cy={p.y} r={spacing * 0.95} fill=\"currentColor\" stroke=\"var(--background)\" strokeWidth=\"2\" />\n          </g>\n        ))}\n      </svg>\n      {labels &&\n        places\n          .filter((p) => p.label)\n          .map((p, i) => (\n            <span\n              key={i}\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute -translate-x-1/2 -translate-y-[calc(100%+0.6rem)] rounded-md border bg-popover px-1.5 py-0.5 text-[11px] font-medium whitespace-nowrap text-popover-foreground shadow-sm\"\n              style={{ left: `${(p.x / WIDTH) * 100}%`, top: `${(p.y / height) * 100}%` }}\n            >\n              {p.label}\n            </span>\n          ))}\n    </div>\n  )\n}\n\nexport { DottedMap, type DottedMapProps, type MapMarker, type MapArc, type MapTone }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/dotted-map.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "map",
      "world",
      "dots",
      "markers",
      "locations"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "<DottedMap markers={[{ lat, lng, label, tone }]} arcs={[{ from:[lat,lng], to:[lat,lng] }]} dots latRange tone labels />. The land mask is built in; nothing is fetched. Higher dots gives finer coastlines.",
      "whenToUse": [
        "Where-we-operate and customer-location sections",
        "Network and traffic visualizations"
      ],
      "whenNotToUse": [
        "Interactive geographic data or zooming (use a map library)",
        "A 3D globe (globe)"
      ],
      "composesWith": [
        "globe",
        "stat-card"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "The map is an image whose name lists every labelled place"
        },
        {
          "keys": "Reduced motion",
          "action": "Routes are drawn solid and markers do not pulse"
        }
      ],
      "customization": [
        "dots",
        "latRange",
        "tone",
        "labels",
        "markers and arcs"
      ]
    },
    "source": {
      "name": "cobe",
      "url": "https://github.com/shuding/cobe",
      "license": "MIT",
      "copyright": "Copyright (c) 2021 Shu Ding",
      "modified": true
    },
    "examples": [
      "dotted-map-demo",
      "dotted-map-regions"
    ],
    "url": "https://ui.ballmac.com/components/dotted-map"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}