{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "motion-dotted-map",
  "title": "Dotted map",
  "description": "A world map drawn in dots from a pre-generated grid, with markers, labels and a slow pulse.",
  "dependencies": [],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "components/core/dotted-map.tsx",
      "content": "import { cn } from '@/lib/utils';\n\nimport {\n  DOTTED_MAP_BOUNDS,\n  DOTTED_MAP_HEIGHT,\n  DOTTED_MAP_PATH,\n  DOTTED_MAP_WIDTH,\n} from './dotted-map-points';\n\nexport type DottedMapMarker = {\n  lat: number;\n  lng: number;\n  label?: string;\n  pulse?: boolean;\n};\n\ntype PlacedMarker = DottedMapMarker & {\n  x: number;\n  y: number;\n  left: string;\n  top: string;\n};\n\nconst EDGE = 0.5;\n\nconst VIEW_WIDTH = DOTTED_MAP_WIDTH + EDGE * 2;\n\nconst VIEW_HEIGHT = DOTTED_MAP_HEIGHT + EDGE * 2;\n\nconst VIEW_BOX = `${-EDGE} ${-EDGE} ${VIEW_WIDTH} ${VIEW_HEIGHT}`;\n\nconst LONGITUDE_SPAN = DOTTED_MAP_BOUNDS.lngMax - DOTTED_MAP_BOUNDS.lngMin;\n\nconst LATITUDE_SPAN = DOTTED_MAP_BOUNDS.latMax - DOTTED_MAP_BOUNDS.latMin;\n\nconst PULSE_STAGGER_MS = 450;\n\nfunction percentOf(value: number, span: number) {\n  const share = (value + EDGE) / span;\n  return `${share * 100}%`;\n}\n\nfunction placeMarker(marker: DottedMapMarker): PlacedMarker {\n  const fromWest = marker.lng - DOTTED_MAP_BOUNDS.lngMin;\n  const fromNorth = DOTTED_MAP_BOUNDS.latMax - marker.lat;\n  const x = (fromWest / LONGITUDE_SPAN) * DOTTED_MAP_WIDTH;\n  const y = (fromNorth / LATITUDE_SPAN) * DOTTED_MAP_HEIGHT;\n  return {\n    ...marker,\n    x,\n    y,\n    left: percentOf(x, VIEW_WIDTH),\n    top: percentOf(y, VIEW_HEIGHT),\n  };\n}\n\nfunction markerKeyOf(marker: DottedMapMarker, index: number) {\n  return `${marker.lat}:${marker.lng}:${index}`;\n}\n\nfunction pulsesOf(marker: DottedMapMarker, pulseByDefault: boolean) {\n  return marker.pulse ?? pulseByDefault;\n}\n\nfunction hasLabel(marker: PlacedMarker) {\n  return Boolean(marker.label);\n}\n\nexport type DottedMapProps = {\n  markers?: DottedMapMarker[];\n  pulse?: boolean;\n  dotSize?: number;\n  markerSize?: number;\n  className?: string;\n  dotClassName?: string;\n  label?: string;\n};\n\nexport function DottedMap({\n  markers = [],\n  pulse = false,\n  dotSize = 0.5,\n  markerSize = 1.1,\n  className,\n  dotClassName,\n  label = 'World map',\n}: DottedMapProps) {\n  const placedMarkers = markers.map(placeMarker);\n  const labelledMarkers = placedMarkers.filter(hasLabel);\n  const markerRadius = markerSize / 2;\n\n  return (\n    <div className={cn('relative w-full', className)}>\n      <svg\n        viewBox={VIEW_BOX}\n        role='img'\n        aria-label={label}\n        className='block h-auto w-full'\n      >\n        <path\n          d={DOTTED_MAP_PATH}\n          fill='none'\n          stroke='currentColor'\n          strokeWidth={dotSize}\n          strokeLinecap='round'\n          strokeDasharray='0 1'\n          className={cn('text-foreground/20', dotClassName)}\n        />\n        {placedMarkers.map((marker, index) => {\n          const isPulsing = pulsesOf(marker, pulse);\n          const pulseDelay = index * PULSE_STAGGER_MS;\n\n          return (\n            <g key={markerKeyOf(marker, index)} className='text-primary'>\n              {isPulsing && (\n                <circle\n                  cx={marker.x}\n                  cy={marker.y}\n                  r={markerRadius}\n                  fill='currentColor'\n                  opacity={0.45}\n                  aria-hidden='true'\n                  style={{\n                    animationDuration: '2.4s',\n                    animationDelay: `${pulseDelay}ms`,\n                  }}\n                  className='origin-center animate-ping [transform-box:fill-box] motion-reduce:hidden'\n                />\n              )}\n              <circle\n                cx={marker.x}\n                cy={marker.y}\n                r={markerRadius}\n                fill='currentColor'\n              />\n            </g>\n          );\n        })}\n      </svg>\n      {labelledMarkers.map((marker, index) => (\n        <span\n          key={markerKeyOf(marker, index)}\n          style={{ left: marker.left, top: marker.top }}\n          className='bg-popover text-popover-foreground hairline-ring ring-border pointer-events-none absolute -mt-2 -translate-x-1/2 -translate-y-full rounded-md px-1.5 py-0.5 text-[11px] leading-4 font-medium whitespace-nowrap shadow-[0_1px_2px_rgb(0_0_0/0.06)]'\n        >\n          {marker.label}\n        </span>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/core/dotted-map.tsx"
    },
    {
      "path": "components/core/dotted-map-points.ts",
      "content": "// Generated by scripts/generate-dotted-map.ts with dotted-map (MIT). Run the script again instead of editing this file.\n// 1534 land dots on a diagonal grid, equirectangular, latitudes -56 to 80.\n\nexport const DOTTED_MAP_WIDTH = 106;\n\nexport const DOTTED_MAP_HEIGHT = 40;\n\nexport const DOTTED_MAP_BOUNDS = {\n  lngMin: -180,\n  lngMax: 180,\n  latMin: -56,\n  latMax: 80,\n} as const;\n\n/** One horizontal run of dots per subpath, one grid unit apart: draw it with a round-capped dash of `0 1`. */\nexport const DOTTED_MAP_PATH =\n  'M25.5 0h6M33.5 0h14M58.5 0h2M80.5 0h1M18 0.87h0M25 0.87h0M27 0.87h2M33 0.87h14M58 0.87h0M83 0.87h1M16.5 1.73h1M25.5 1.73h0M36.5 1.73h10M69.5 1.73h0M78.5 1.73h6M7 2.6h0M17 2.6h0M19 2.6h3M25 2.6h0M27 2.6h4M38 2.6h8M69 2.6h0M73 2.6h24M0.5 3.46h0M4.5 3.46h14M21.5 3.46h0M24.5 3.46h2M28.5 3.46h0M31.5 3.46h1M37.5 3.46h6M58.5 3.46h6M66.5 3.46h0M68.5 3.46h37M2 4.33h0M4 4.33h23M31 4.33h3M38 4.33h3M47 4.33h1M57 4.33h2M61 4.33h2M65 4.33h40M4.5 5.2h21M28.5 5.2h2M38.5 5.2h1M55.5 5.2h2M59.5 5.2h45M6 6.06h1M12 6.06h13M31 6.06h1M34 6.06h0M55 6.06h3M61 6.06h35M98 6.06h0M101 6.06h0M14.5 6.93h12M30.5 6.93h4M51.5 6.93h0M55.5 6.93h0M57.5 6.93h0M59.5 6.93h34M99.5 6.93h1M14 7.79h14M30 7.79h6M51 7.79h0M53 7.79h0M56 7.79h39M99 7.79h1M15.5 8.66h19M51.5 8.66h0M53.5 8.66h40M17 9.53h16M36 9.53h0M53 9.53h40M95 9.53h0M16.5 10.39h16M53.5 10.39h7M64.5 10.39h2M68.5 10.39h24M17 11.26h15M51 11.26h2M57 11.26h0M59 11.26h2M63 11.26h0M66 11.26h1M69 11.26h22M17.5 12.12h12M50.5 12.12h2M59.5 12.12h0M61.5 12.12h5M69.5 12.12h18M90.5 12.12h0M94.5 12.12h0M18 12.99h12M53 12.99h3M64 12.99h24M91 12.99h0M94 12.99h0M18.5 13.86h11M50.5 13.86h6M63.5 13.86h25M91.5 13.86h0M19 14.72h7M28 14.72h1M51 14.72h37M19.5 15.59h0M21.5 15.59h2M49.5 15.59h13M64.5 15.59h3M70.5 15.59h17M22 16.45h2M49 16.45h14M65 16.45h4M73 16.45h14M22.5 17.32h1M30.5 17.32h0M48.5 17.32h15M65.5 17.32h4M74.5 17.32h4M80.5 17.32h5M23 18.19h4M30 18.19h0M32 18.19h0M49 18.19h15M66 18.19h3M75 18.19h2M81 18.19h3M89 18.19h0M26.5 19.05h1M48.5 19.05h16M66.5 19.05h1M75.5 19.05h1M82.5 19.05h2M88.5 19.05h0M28 19.92h0M32 19.92h0M49 19.92h16M76 19.92h0M82 19.92h0M84 19.92h1M28.5 20.78h1M31.5 20.78h3M49.5 20.78h18M31 21.65h5M50 21.65h17M83 21.65h0M30.5 22.52h7M56.5 22.52h10M86.5 22.52h1M30 23.38h8M56 23.38h9M83 23.38h0M86 23.38h1M89 23.38h0M29.5 24.25h10M56.5 24.25h8M83.5 24.25h0M85.5 24.25h1M88.5 24.25h0M92.5 24.25h1M30 25.11h12M57 25.11h7M84 25.11h0M94 25.11h1M30.5 25.98h12M57.5 25.98h7M86.5 25.98h0M88.5 25.98h0M94.5 25.98h0M96.5 25.98h0M31 26.85h11M58 26.85h6M92 26.85h0M95 26.85h0M31.5 27.71h10M57.5 27.71h7M67.5 27.71h0M90.5 27.71h2M32 28.58h9M57 28.58h7M66 28.58h1M90 28.58h3M95 28.58h0M32.5 29.44h8M57.5 29.44h5M66.5 29.44h0M88.5 29.44h8M33 30.31h7M58 30.31h5M66 30.31h1M87 30.31h10M32.5 31.18h6M57.5 31.18h5M86.5 31.18h11M32 32.04h6M58 32.04h4M87 32.04h11M32.5 32.91h5M58.5 32.91h3M87.5 32.91h3M92.5 32.91h5M32 33.77h3M94 33.77h3M104 33.77h0M31.5 34.64h4M94.5 34.64h2M104.5 34.64h1M32 35.51h2M31.5 36.37h2M103.5 36.37h0M31 37.24h2M31.5 38.11h1M73.5 38.11h0M32 38.97h0';\n",
      "type": "registry:component",
      "target": "components/core/dotted-map-points.ts"
    }
  ],
  "categories": [
    "motion"
  ],
  "type": "registry:component"
}