{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-map",
  "type": "registry:component",
  "title": "Dot Map",
  "description": "A dotted world map drawn from a hand-made land mask, with one region lit up, live sessions pulsing across it and a switcher that moves the spotlight.",
  "categories": [
    "illustration",
    "Connections"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/segmented-control.json"
  ],
  "files": [
    {
      "path": "registry/devl/dot-map.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useInView, useReducedMotion, useSpring, useTransform } from \"motion/react\";\nimport { type ComponentProps, memo, useEffect, useMemo, useRef, useState } from \"react\";\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ui/segmented-control\";\nimport { cn } from \"@/lib/utils\";\n\nexport type WorldRegion = \"americas\" | \"europe\" | \"africa\" | \"asia\" | \"oceania\";\n\nexport interface WorldCity {\n  name: string;\n  lon: number;\n  lat: number;\n  region: WorldRegion;\n}\n\nexport interface DotMapProps extends Omit<ComponentProps<\"div\">, \"children\" | \"onChange\"> {\n  region?: WorldRegion;\n  onRegionChange?: (region: WorldRegion) => void;\n  defaultRegion?: WorldRegion;\n  autoCycle?: boolean;\n  cities?: WorldCity[];\n  counts?: Record<WorldRegion, number>;\n  title?: string;\n}\n\nconst LAND_ROWS = [\n  \"a34.33 e77.5\",\n  \"a34.8 a43.22 e76.4 s98.1 s112.3\",\n  \"a35.5 a47.18 e95.1 s96.2 s106.11\",\n  \"a21.9 a35.7 a49.15 e92.3 s98.25 s126.5\",\n  \"a6.11 a18.14 a34.2 a37.8 a50.13 e79.7 s96.47\",\n  \"s0.3 a4.35 a41.6 a50.10 e77.18 s95.48\",\n  \"s0.4 a5.34 a41.6 a51.5 e62.4 e76.20 s96.48\",\n  \"a5.30 a38.1 a40.6 a51.4 e73.6 e80.15 s95.48\",\n  \"a6.29 a41.3 a46.1 a53.2 e74.6 e81.15 s96.38 s135.6\",\n  \"a8.2 a17.18 a41.6 e69.2 e74.4 e80.15 s95.33 s134.3\",\n  \"a7.1 a19.20 a40.8 e68.4 e75.21 s96.31 s134.3\",\n  \"a20.29 e67.5 e73.22 s95.34 s134.1\",\n  \"a21.30 e70.26 s96.34\",\n  \"a21.25 a48.3 e70.20 s90.39\",\n  \"a22.26 e71.13 e85.5 s90.1 s92.35 s128.2\",\n  \"a21.23 e68.6 e76.7 e85.1 e88.2 s90.1 s92.33 s127.3\",\n  \"a22.21 e68.4 e75.1 e78.1 e80.1 s82.10 s93.31 s128.1\",\n  \"a22.20 e68.3 f73.3 e77.1 e80.1 s82.38 s121.3 s126.2\",\n  \"a23.19 f69.8 s85.35 s122.6\",\n  \"a24.16 f68.10 f79.3 s85.35 s123.2\",\n  \"a25.15 f68.17 s85.36\",\n  \"a26.7 a38.2 f66.19 s85.35\",\n  \"a27.6 a39.1 f66.20 s87.6 s94.26\",\n  \"a29.4 a38.3 f65.21 s87.9 s99.19 s119.1\",\n  \"a30.4 a35.3 a40.3 f65.23 s88.8 s101.6 s109.7\",\n  \"a30.7 a42.2 f64.23 s88.6 s100.5 s109.5 s119.2\",\n  \"a34.5 f65.23 s89.4 s101.4 s111.5 s120.1\",\n  \"a36.2 f65.24 s89.1 s101.3 s111.4 s120.2\",\n  \"a37.2 a40.8 f66.27 s102.2 s111.1 s113.2 s120.3\",\n  \"a38.10 f66.26 s103.1 s111.1 s120.2\",\n  \"a41.10 f68.4 f74.18 s110.1 s112.2 s118.2\",\n  \"a40.12 f75.15 s110.3 s116.3 s122.1\",\n  \"a39.14 f75.15 s112.1 s115.4 s120.1 s123.1\",\n  \"a39.17 f75.13 s112.2 s115.3 s119.2 s123.1 o124.5\",\n  \"a39.19 f76.12 s113.2 s119.2 o126.5\",\n  \"a39.19 f76.12 s114.7 o127.4\",\n  \"a40.18 f77.11 o131.1\",\n  \"a40.17 f76.12 f91.1 o124.2 o128.1\",\n  \"a42.15 f77.12 f90.2 o122.5 o128.2\",\n  \"a42.14 f76.11 f89.3 o120.10\",\n  \"a44.12 f77.9 f89.3 o119.13\",\n  \"a43.12 f77.9 f89.2 o117.15\",\n  \"a43.11 f78.8 f90.1 o117.16\",\n  \"a43.9 f78.7 o117.16\",\n  \"a43.9 f79.6 o118.16\",\n  \"a43.8 f78.5 o117.6 o124.9\",\n  \"a43.7 f79.3 o118.2 o127.6\",\n  \"a42.7 o127.5 o141.2\",\n  \"a42.5 o130.1 o141.2\",\n  \"a42.4 o129.2 o139.2\",\n  \"a42.4 o138.3\",\n  \"a41.4\",\n  \"a42.3\",\n  \"a42.2\",\n  \"a44.1\",\n];\n\nconst STEP = 2.5;\nconst TOP = 80;\nconst NORTH = 81.5;\nconst SOUTH = -56.5;\nconst W = 560;\nconst H = Math.round((W * (NORTH - SOUTH)) / 360);\nconst REGION_CODE: Record<string, WorldRegion> = { a: \"americas\", e: \"europe\", f: \"africa\", s: \"asia\", o: \"oceania\" };\n\nfunction px(lon: number, lat: number) {\n  return { x: ((lon + 180) / 360) * W, y: ((NORTH - lat) / (NORTH - SOUTH)) * H };\n}\n\nfunction hash(n: number) {\n  let h = Math.imul(n ^ 0x9e3779b9, 0x85ebca6b);\n  h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);\n  return ((h ^ (h >>> 16)) >>> 0) / 4294967296;\n}\n\nconst round = (v: number) => Math.round(v * 100) / 100;\n\nfunction buildDots() {\n  const dots: { x: number; y: number; region: WorldRegion; o: number }[] = [];\n  LAND_ROWS.forEach((row, r) => {\n    const lat = TOP - r * STEP;\n    const shift = r % 2 ? STEP / 2 : 0;\n    for (const run of row.split(\" \").filter(Boolean)) {\n      const region = REGION_CODE[run[0]];\n      const [first, count] = run.slice(1).split(\".\").map(Number);\n      for (let k = first; k < first + count; k++) {\n        const { x, y } = px(-180 + STEP / 2 + shift + k * STEP, lat);\n        dots.push({ x: round(x), y: round(y), region, o: round(0.55 + hash(dots.length) * 0.45) });\n      }\n    }\n  });\n  return dots;\n}\n\nconst defaultCities: WorldCity[] = [\n  { name: \"San Francisco\", lon: -122.4, lat: 37.8, region: \"americas\" },\n  { name: \"New York\", lon: -74, lat: 40.7, region: \"americas\" },\n  { name: \"Toronto\", lon: -79.4, lat: 43.7, region: \"americas\" },\n  { name: \"Mexico City\", lon: -99.1, lat: 19.4, region: \"americas\" },\n  { name: \"Bogotá\", lon: -74.1, lat: 4.7, region: \"americas\" },\n  { name: \"São Paulo\", lon: -46.6, lat: -23.5, region: \"americas\" },\n  { name: \"Buenos Aires\", lon: -58.4, lat: -34.6, region: \"americas\" },\n  { name: \"London\", lon: -0.1, lat: 51.5, region: \"europe\" },\n  { name: \"Paris\", lon: 2.3, lat: 48.9, region: \"europe\" },\n  { name: \"Berlin\", lon: 13.4, lat: 52.5, region: \"europe\" },\n  { name: \"Madrid\", lon: -3.7, lat: 40.4, region: \"europe\" },\n  { name: \"Stockholm\", lon: 18.1, lat: 59.3, region: \"europe\" },\n  { name: \"Warsaw\", lon: 21, lat: 52.2, region: \"europe\" },\n  { name: \"Lagos\", lon: 3.4, lat: 6.5, region: \"africa\" },\n  { name: \"Nairobi\", lon: 36.8, lat: -1.3, region: \"africa\" },\n  { name: \"Cairo\", lon: 31.2, lat: 30, region: \"africa\" },\n  { name: \"Johannesburg\", lon: 28, lat: -26.2, region: \"africa\" },\n  { name: \"Casablanca\", lon: -7.6, lat: 33.6, region: \"africa\" },\n  { name: \"Tokyo\", lon: 139.7, lat: 35.7, region: \"asia\" },\n  { name: \"Singapore\", lon: 103.8, lat: 1.4, region: \"asia\" },\n  { name: \"Mumbai\", lon: 72.9, lat: 19.1, region: \"asia\" },\n  { name: \"Seoul\", lon: 127, lat: 37.6, region: \"asia\" },\n  { name: \"Dubai\", lon: 55.3, lat: 25.2, region: \"asia\" },\n  { name: \"Shanghai\", lon: 121.5, lat: 31.2, region: \"asia\" },\n  { name: \"Bengaluru\", lon: 77.6, lat: 13, region: \"asia\" },\n  { name: \"Sydney\", lon: 151.2, lat: -33.9, region: \"oceania\" },\n  { name: \"Melbourne\", lon: 145, lat: -37.8, region: \"oceania\" },\n  { name: \"Auckland\", lon: 174.8, lat: -36.8, region: \"oceania\" },\n  { name: \"Perth\", lon: 115.9, lat: -31.9, region: \"oceania\" },\n  { name: \"Brisbane\", lon: 153, lat: -27.5, region: \"oceania\" },\n];\n\nconst defaultCounts: Record<WorldRegion, number> = {\n  americas: 3412,\n  europe: 2876,\n  africa: 941,\n  asia: 4128,\n  oceania: 612,\n};\n\nconst regions: { id: WorldRegion; label: string }[] = [\n  { id: \"americas\", label: \"Americas\" },\n  { id: \"europe\", label: \"Europe\" },\n  { id: \"africa\", label: \"Africa\" },\n  { id: \"asia\", label: \"Asia\" },\n  { id: \"oceania\", label: \"Oceania\" },\n];\n\nconst DotField = memo(function DotField({ region }: { region: WorldRegion }) {\n  const dots = useMemo(buildDots, []);\n  return (\n    <g>\n      {dots.map((d, i) => {\n        const on = d.region === region;\n        return (\n          <circle\n            className=\"transition-[fill,fill-opacity] duration-500\"\n            cx={d.x}\n            cy={d.y}\n            fill={on ? \"var(--color-info)\" : \"var(--color-foreground)\"}\n            fillOpacity={on ? d.o : 0.16}\n            key={i}\n            r={on ? 1.35 : 1.1}\n          />\n        );\n      })}\n    </g>\n  );\n});\n\ninterface Pulse {\n  id: number;\n  city: WorldCity;\n  on: boolean;\n}\n\nfunction RollingNumber({ value }: { value: number }) {\n  const spring = useSpring(value, { stiffness: 90, damping: 20 });\n  const text = useTransform(spring, (v) => Math.round(v).toLocaleString(\"en-US\"));\n  useEffect(() => spring.set(value), [spring, value]);\n  return <motion.span>{text}</motion.span>;\n}\n\nexport function DotMap({\n  region: regionProp,\n  onRegionChange,\n  defaultRegion = \"europe\",\n  autoCycle = true,\n  cities = defaultCities,\n  counts = defaultCounts,\n  title = \"Live sessions\",\n  className,\n  ...props\n}: DotMapProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const inView = useInView(ref);\n  const reduce = useReducedMotion();\n  const [inner, setInner] = useState<WorldRegion>(defaultRegion);\n  const [touched, setTouched] = useState(false);\n  const region = regionProp ?? inner;\n  const [pulses, setPulses] = useState<Pulse[]>([]);\n  const [drift, setDrift] = useState(0);\n  const seq = useRef(0);\n  const live = inView && !reduce;\n\n  function pick(next: WorldRegion) {\n    setTouched(true);\n    setInner(next);\n    onRegionChange?.(next);\n  }\n\n  useEffect(() => {\n    if (!live || !autoCycle || touched || regionProp) return;\n    const id = setInterval(() => {\n      setInner((r) => regions[(regions.findIndex((x) => x.id === r) + 1) % regions.length].id);\n    }, 6000);\n    return () => clearInterval(id);\n  }, [live, autoCycle, touched, regionProp]);\n\n  useEffect(() => {\n    if (!live) return;\n    const local = cities.filter((c) => c.region === region);\n    const away = cities.filter((c) => c.region !== region);\n    const id = setInterval(() => {\n      seq.current += 1;\n      const n = seq.current;\n      const on = local.length > 0 && (away.length === 0 || n % 3 !== 0);\n      const pool = on ? local : away;\n      const city = pool[Math.floor(Math.random() * pool.length)];\n      if (!city) return;\n      setPulses((p) => [...p.filter((x) => n - x.id < 4), { id: n, city, on }]);\n      if (on) setDrift((d) => Math.max(-40, Math.min(40, d + Math.round(Math.random() * 9 - 3))));\n    }, 750);\n    return () => clearInterval(id);\n  }, [live, region, cities]);\n\n  useEffect(() => setDrift(0), [region]);\n\n  const latest = [...pulses].reverse().find((p) => p.on && p.city.region === region);\n  const label = regions.find((r) => r.id === region)?.label;\n\n  return (\n    <div\n      className={cn(\"w-full max-w-[560px] overflow-hidden rounded-2xl border bg-card shadow-xs/5\", className)}\n      ref={ref}\n      {...props}\n    >\n      <div className=\"flex items-end justify-between gap-4 px-5 pt-4\">\n        <div className=\"min-w-0\">\n          <p className=\"flex items-center gap-1.5 text-muted-foreground text-xs\">\n            <span className=\"relative flex size-1.5\">\n              {live && <span className=\"absolute inset-0 animate-ping rounded-full bg-success opacity-70\" />}\n              <span className=\"relative size-1.5 rounded-full bg-success\" />\n            </span>\n            {title}\n          </p>\n          <p className=\"mt-1 font-medium text-2xl tabular-nums tracking-tight\">\n            <RollingNumber value={counts[region] + drift} />\n          </p>\n        </div>\n        <div className=\"relative h-5 min-w-0 flex-1 overflow-hidden text-right text-muted-foreground text-xs\">\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            {latest && (\n              <motion.p\n                animate={{ y: 0, opacity: 1 }}\n                className=\"truncate\"\n                exit={{ y: -14, opacity: 0 }}\n                initial={{ y: 14, opacity: 0 }}\n                key={latest.id}\n                transition={{ type: \"spring\", stiffness: 300, damping: 28 }}\n              >\n                New session in <span className=\"text-foreground\">{latest.city.name}</span>\n              </motion.p>\n            )}\n          </AnimatePresence>\n        </div>\n      </div>\n\n      <div className=\"px-3 pt-3 pb-2\">\n        <svg aria-label={`Map of live sessions, ${label} highlighted`} className=\"block h-auto w-full\" role=\"img\" viewBox={`0 0 ${W} ${H}`}>\n          <DotField region={region} />\n          <AnimatePresence>\n            {pulses.map((p) => {\n              const { x, y } = px(p.city.lon, p.city.lat);\n              const on = p.city.region === region;\n              const color = on ? \"var(--color-info)\" : \"var(--color-foreground)\";\n              return (\n                <motion.g exit={{ opacity: 0 }} key={p.id} transition={{ duration: 0.6 }}>\n                  <motion.circle\n                    animate={{ scale: 1, opacity: 0 }}\n                    cx={x}\n                    cy={y}\n                    fill=\"none\"\n                    initial={{ scale: 0.15, opacity: on ? 0.9 : 0.4 }}\n                    r={on ? 16 : 10}\n                    stroke={color}\n                    strokeWidth={1.25}\n                    style={{ transformBox: \"fill-box\", transformOrigin: \"center\" }}\n                    transition={{ duration: 2, ease: [0.23, 1, 0.32, 1] }}\n                    vectorEffect=\"non-scaling-stroke\"\n                  />\n                  <motion.circle\n                    animate={{ scale: 1, opacity: on ? 1 : 0.5 }}\n                    cx={x}\n                    cy={y}\n                    fill={color}\n                    initial={{ scale: 0.5, opacity: 0 }}\n                    r={on ? 2.6 : 1.8}\n                    style={{ transformBox: \"fill-box\", transformOrigin: \"center\" }}\n                    transition={{ type: \"spring\", stiffness: 420, damping: 26 }}\n                  />\n                  {on && <circle cx={x} cy={y} fill=\"var(--color-info)\" opacity={0.25} r={6} />}\n                </motion.g>\n              );\n            })}\n          </AnimatePresence>\n        </svg>\n      </div>\n\n      <div className=\"flex justify-center border-t px-3 py-3\">\n        <SegmentedControl aria-label=\"Region\" className=\"max-w-full\" size=\"sm\">\n          {regions.map((r) => (\n            <SegmentedControlItem\n              aria-pressed={region === r.id}\n              className=\"px-2 text-muted-foreground text-xs focus-visible:ring-2 focus-visible:ring-ring sm:px-2.5 sm:text-xs\"\n              data-pressed={region === r.id ? \"\" : undefined}\n              key={r.id}\n              onClick={() => pick(r.id)}\n            >\n              {r.label}\n            </SegmentedControlItem>\n          ))}\n        </SegmentedControl>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}