{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "geo-region-picker",
  "title": "Region picker",
  "description": "Pick a deployment region from a list with a dot map. The reticle travels along the great circle, magnifying the land dots it passes.",
  "registryDependencies": [
    "https://elements.lumesec.ai/r/lumesec-theme.json",
    "https://elements.lumesec.ai/r/lumesec-canvas.json",
    "https://elements.lumesec.ai/r/lumesec-color.json",
    "https://elements.lumesec.ai/r/lumesec-geo.json",
    "https://elements.lumesec.ai/r/lumesec-geo-data.json",
    "https://elements.lumesec.ai/r/lumesec-math.json",
    "https://elements.lumesec.ai/r/lumesec-motion.json",
    "https://elements.lumesec.ai/r/lumesec-roll-text.json",
    "https://elements.lumesec.ai/r/lumesec-sparks.json",
    "https://elements.lumesec.ai/r/lumesec-use-controllable-state.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/components/lumesec/geo-region-picker.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { fillSquare, useCanvas, type CanvasSurface } from \"@/lib/lumesec/canvas\";\nimport { usePalette } from \"@/lib/lumesec/color\";\nimport {\n  buildArcLength,\n  createProjection,\n  createScreenPoint,\n  cubicBezier,\n  distanceKm,\n  fiberRttMs,\n  flatPaths,\n  forEachDot,\n  GEO_TOKENS,\n  geoInk,\n  interpolateGreatCircle,\n  mapLattice,\n  projectionAspect,\n  projectionOutlineSvgPath,\n  spatialNavigate,\n  unwrapLongitude,\n  wrapLongitude,\n  type ArcLengthTable,\n  type DotLattice,\n  type GeoInk,\n  type LatLon,\n  type MapProjection,\n} from \"@/lib/lumesec/geo\";\nimport { DEMO_REGIONS, formatNumber, type GeoRegion } from \"@/lib/lumesec/geo-data\";\nimport { clamp, mixRgb, percent, rgba, smoothstep, TAU } from \"@/lib/lumesec/math\";\nimport { prefersReducedMotion, springStep, useAnimationLoop, useLazyRef } from \"@/lib/lumesec/motion\";\nimport { RollText } from \"@/lib/lumesec/roll-text\";\nimport { Sparks, type SparkOptions } from \"@/lib/lumesec/sparks\";\nimport { useControllableState, useFormReset } from \"@/lib/lumesec/use-controllable-state\";\nimport { cn } from \"@/lib/utils\";\n\n/** A region to choose from: `{ id, label, city, timeZone, lat, lon }`, re-exported from the geo data module. */\nexport type { GeoRegion } from \"@/lib/lumesec/geo-data\";\n\n/** Props of `GeoRegionPicker`; the remaining `div` props go to the root. */\nexport interface GeoRegionPickerProps extends Omit<React.ComponentProps<\"div\">, \"defaultValue\"> {\n  /** Selected region id (controlled). Use with `onValueChange`. An id that is not in `regions` selects nothing. */\n  value?: string;\n  /** Selected region id when uncontrolled. */\n  defaultValue?: string;\n  /** Called with the region id when the user picks a region. */\n  onValueChange?: (value: string) => void;\n  /**\n   * Regions to choose from, in list order: `{ id, label, city, timeZone, lat, lon }`. Regions with a repeated id or a\n   * non-finite position are skipped. Without it the picker shows the 16 demo regions. An empty array shows\n   * \"No regions\".\n   */\n  regions?: readonly GeoRegion[];\n  /** Location the latency hints are measured from, with the label its map point shows (default `From`). */\n  from?: LatLon & { label?: string };\n  /** How much longer the network path is than the straight fibre distance, for the hints. */\n  stretch?: number;\n  /** Which parts are shown: the map above the list, the map with a compact wrap of chips, or the list alone. */\n  layout?: \"both\" | \"map\" | \"list\";\n  /** Form field name. With a name and a selected region, a hidden input submits the region id. */\n  name?: string;\n  /** Marks the group as required (`aria-required`). */\n  required?: boolean;\n  /** Dims the picker and takes it out of the tab order. */\n  disabled?: boolean;\n  /** Visible heading of the list, and the accessible name of the radiogroup. */\n  label?: string;\n}\n\n/**\n * Pick a deployment region from a list with a dot map above it. The selection reticle travels to the new region\n * along the great-circle path on a spring (long trips take longer), trailing a short comet, and a lens magnifies the\n * land dots under it as it passes. At rest the lens stays on the selected region.\n *\n * With `from`, every region shows an estimated round trip, and pointing at a row or a map point draws a dotted line\n * from that location to the region. The list is a radiogroup with one tab stop: Up and Down follow the list, Left and\n * Right jump to the nearest region west or east on the map.\n *\n * @example\n * <GeoRegionPicker name=\"region\" defaultValue=\"eu-west\" from={{ lat: 40.42, lon: -3.7, label: \"Madrid office\" }} />\n */\nexport function GeoRegionPicker({\n  value,\n  defaultValue = \"eu-central\",\n  onValueChange,\n  regions,\n  from,\n  stretch = 1.5,\n  layout = \"both\",\n  name,\n  required = false,\n  disabled = false,\n  label = \"Region\",\n  ref,\n  className,\n  ...props\n}: GeoRegionPickerProps) {\n  const list = React.useMemo(() => usableRegions(regions ?? DEMO_REGIONS), [regions]);\n  const [selectedId, setSelectedId] = useControllableState<string>({ value, defaultValue, onChange: onValueChange });\n  const selectedIndex = list.findIndex((region) => region.id === selectedId);\n  const selected = selectedIndex >= 0 ? list[selectedIndex] : null;\n  const labelId = React.useId();\n\n  const origin = from && Number.isFinite(from.lat) && Number.isFinite(from.lon) ? from : null;\n  const fromLat = origin?.lat;\n  const fromLon = origin?.lon;\n  const fromLabel = origin?.label || \"From\";\n  const factor = Number.isFinite(stretch) && stretch > 0 ? stretch : 1.5;\n\n  const [rowHover, setRowHover] = React.useState<number | null>(null);\n  const [mapHover, setMapHover] = React.useState<number | null>(null);\n  const [focusPreview, setFocusPreview] = React.useState<number | null>(null);\n  const previewCandidate = rowHover ?? mapHover ?? focusPreview;\n  const previewIndex = previewCandidate !== null && previewCandidate < list.length ? previewCandidate : null;\n  const hoverRegion = mapHover !== null ? list[mapHover] : undefined;\n\n  /** Region positions on the nominal 1000 px map: SSR-safe percentages and the spatial keyboard order. */\n  const points = React.useMemo(() => list.map((region) => nominalPoint(region)), [list]);\n  const hints = React.useMemo(() => {\n    if (fromLat === undefined || fromLon === undefined) return null;\n    const start: LatLon = { lat: fromLat, lon: fromLon };\n    return list.map((region) => roundTripMs(start, region, factor));\n  }, [list, fromLat, fromLon, factor]);\n  const fromPoint = React.useMemo(\n    () => (fromLat === undefined || fromLon === undefined ? null : nominalPoint({ lat: fromLat, lon: fromLon })),\n    [fromLat, fromLon],\n  );\n  const fromSide = React.useMemo(\n    () => (fromPoint ? labelSide(fromPoint, fromLabel, points) : \"right\"),\n    [fromPoint, fromLabel, points],\n  );\n\n  // The server markup places the reticle with `left` and `top`; the frame loop moves it from there with a transform.\n  const [reticleOrigin] = React.useState(() => (selectedIndex >= 0 ? points[selectedIndex] : MAP_MIDDLE));\n\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const mapRef = React.useRef<HTMLDivElement>(null);\n  const baseRef = React.useRef<HTMLCanvasElement>(null);\n  const marksRef = React.useRef<HTMLCanvasElement>(null);\n  const effectsRef = React.useRef<HTMLCanvasElement>(null);\n  const reticleRef = React.useRef<HTMLDivElement>(null);\n  const bracketsRef = React.useRef<HTMLDivElement>(null);\n  const groupRef = React.useRef<HTMLDivElement>(null);\n  /** The map engine, created on first use in an effect, handler or frame (never during render). */\n  const engineRef = useLazyRef(() => new RegionMap());\n  /** The first scroll of the list jumps; later ones glide. */\n  const scrolledRef = React.useRef(false);\n\n  React.useImperativeHandle(ref, () => rootRef.current!, []);\n\n  /** Where the map's top-left corner sits on the effects layer, in CSS px (offsets ignore the gallery's zoom). */\n  function measureEffects() {\n    const layer = effectsRef.current;\n    if (layer) engineRef.current.setEffectsOffset(-layer.offsetLeft, -layer.offsetTop);\n  }\n\n  const loop = useAnimationLoop(rootRef, step);\n  const palette = usePalette(rootRef, GEO_TOKENS, applyTheme);\n  const base = useCanvas(\n    baseRef,\n    (surface) => {\n      engineRef.current.layout(surface.width, surface.height, surface.dpr);\n      loop.kick();\n    },\n    MAP_CANVAS,\n  );\n  const marks = useCanvas(\n    marksRef,\n    () => {\n      engineRef.current.invalidate();\n      loop.kick();\n    },\n    MAP_CANVAS,\n  );\n  const effects = useCanvas(\n    effectsRef,\n    () => {\n      measureEffects();\n      loop.kick();\n    },\n    MAP_CANVAS,\n  );\n\n  function step(_time: number, delta: number) {\n    const engine = engineRef.current;\n    const moving = engine.step(base.current, marks.current, effects.current, delta);\n    placeReticle(engine);\n    return moving;\n  }\n\n  /** Moves the DOM reticle to the engine's point, relative to where the server markup put it. */\n  function placeReticle(engine: RegionMap) {\n    const node = reticleRef.current;\n    const box = engine.size;\n    const point = engine.reticle;\n    if (!node || !box || !point.shown) return;\n    const dx = point.x - (reticleOrigin.x / NOMINAL_WIDTH) * box.width;\n    const dy = point.y - (reticleOrigin.y / NOMINAL_HEIGHT) * box.height;\n    node.style.transform = `translate3d(${dx.toFixed(2)}px,${dy.toFixed(2)}px,0)`;\n  }\n\n  function applyTheme() {\n    const colors = palette.current;\n    if (!colors) return;\n    engineRef.current.setInk(geoInk(colors));\n    loop.kick();\n  }\n\n  useFormReset(rootRef, () => setSelectedId(defaultValue));\n\n  // The engine reports the reticle's arrival from the frame loop: the brackets snap in.\n  React.useEffect(() => {\n    engineRef.current.onArrive = () => {\n      const node = bracketsRef.current;\n      if (!node || typeof node.animate !== \"function\" || prefersReducedMotion()) return;\n      node.animate(SNAP_KEYFRAMES, SNAP_TIMING);\n    };\n  });\n\n  React.useEffect(() => {\n    engineRef.current.setRegions(list, selectedIndex);\n    loop.kick();\n  }, [list, selectedIndex, engineRef, loop]);\n\n  React.useEffect(() => {\n    engineRef.current.setSelected(selectedIndex, !prefersReducedMotion());\n    loop.kick();\n  }, [selectedIndex, engineRef, loop]);\n\n  React.useEffect(() => {\n    engineRef.current.setFrom(fromLat === undefined || fromLon === undefined ? null : { lat: fromLat, lon: fromLon });\n    loop.kick();\n  }, [fromLat, fromLon, engineRef, loop]);\n\n  React.useEffect(() => {\n    engineRef.current.setPreview(previewIndex ?? -1);\n    loop.kick();\n  }, [previewIndex, engineRef, loop]);\n\n  // Keep the selected row inside the scrolling list, whichever way it was picked.\n  React.useEffect(() => {\n    const group = groupRef.current;\n    if (!group || selectedIndex < 0 || group.scrollHeight <= group.clientHeight) return;\n    const row = group.querySelector<HTMLElement>(`[data-index=\"${selectedIndex}\"]`);\n    if (!row) return;\n    const top = row.offsetTop - SCROLL_MARGIN;\n    const bottom = row.offsetTop + row.offsetHeight + SCROLL_MARGIN;\n    let next = group.scrollTop;\n    if (top < group.scrollTop) next = top;\n    else if (bottom > group.scrollTop + group.clientHeight) next = bottom - group.clientHeight;\n    const glide = scrolledRef.current && !prefersReducedMotion();\n    scrolledRef.current = true;\n    if (Math.abs(next - group.scrollTop) > 0.5) group.scrollTo({ top: next, behavior: glide ? \"smooth\" : \"auto\" });\n  }, [selectedIndex, layout]);\n\n  function select(index: number) {\n    const region = list[index];\n    if (disabled || !region) return;\n    setSelectedId(region.id);\n  }\n\n  function focusRadio(index: number) {\n    groupRef.current?.querySelector<HTMLElement>(`[data-index=\"${index}\"]`)?.focus({ preventScroll: true });\n  }\n\n  function handleGroupKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n    if (disabled || list.length === 0) return;\n    const radio = event.target instanceof HTMLElement ? event.target.closest<HTMLElement>(\"[data-index]\") : null;\n    const index = radio ? Number(radio.dataset.index) : NaN;\n    if (!Number.isInteger(index) || index < 0 || index >= list.length) return;\n    const last = list.length - 1;\n    let next: number;\n    switch (event.key) {\n      case \"ArrowDown\":\n        next = index >= last ? 0 : index + 1;\n        break;\n      case \"ArrowUp\":\n        next = index <= 0 ? last : index - 1;\n        break;\n      case \"ArrowRight\":\n        next = neighbourOnMap(points, index, \"right\");\n        break;\n      case \"ArrowLeft\":\n        next = neighbourOnMap(points, index, \"left\");\n        break;\n      case \"Home\":\n        next = 0;\n        break;\n      case \"End\":\n        next = last;\n        break;\n      default:\n        return;\n    }\n    event.preventDefault();\n    select(next);\n    if (next !== index) focusRadio(next);\n  }\n\n  /** The region nearest to a pointer position on the map, within the hit radius, or −1. */\n  function regionAt(clientX: number, clientY: number) {\n    const box = mapRef.current;\n    if (!box || !box.offsetWidth) return -1;\n    const rect = box.getBoundingClientRect();\n    if (!rect.width || !rect.height) return -1;\n    // nominal map units, so the gallery's zoom does not matter\n    const x = ((clientX - rect.left) / rect.width) * NOMINAL_WIDTH;\n    const y = ((clientY - rect.top) / rect.height) * NOMINAL_HEIGHT;\n    const radius = (HIT_RADIUS / box.offsetWidth) * NOMINAL_WIDTH;\n    let best = -1;\n    let bestDistance = radius * radius;\n    points.forEach((point, i) => {\n      const distance = (point.x - x) ** 2 + (point.y - y) ** 2;\n      if (distance <= bestDistance) {\n        bestDistance = distance;\n        best = i;\n      }\n    });\n    return best;\n  }\n\n  function handleMapPointerMove(event: React.PointerEvent<HTMLDivElement>) {\n    if (event.pointerType === \"touch\" || disabled) return;\n    const index = regionAt(event.clientX, event.clientY);\n    setMapHover(index >= 0 ? index : null);\n  }\n\n  /** A press on a map target picks the region nearest to the pointer (targets of close regions overlap). */\n  function handleMarkerClick(event: React.MouseEvent<HTMLButtonElement>, own: number) {\n    const index = regionAt(event.clientX, event.clientY);\n    const pick = index >= 0 ? index : own;\n    select(pick);\n    focusRadio(pick);\n  }\n\n  function radioProps(region: GeoRegion, i: number) {\n    const checked = i === selectedIndex;\n    const tabStop = selectedIndex >= 0 ? selectedIndex : 0;\n    const hint = hints ? hints[i] : null;\n    return {\n      type: \"button\" as const,\n      role: \"radio\",\n      \"aria-checked\": checked,\n      \"aria-label\": `${region.label}, ${region.city}${hint !== null ? `, about ${spokenMs(hint)} round trip` : \"\"}`,\n      \"data-index\": i,\n      \"data-state\": checked ? \"checked\" : \"unchecked\",\n      \"data-active\": i === previewIndex ? \"\" : undefined,\n      tabIndex: disabled || i !== tabStop ? -1 : 0,\n      disabled,\n      onClick: () => select(i),\n      onPointerEnter: (event: React.PointerEvent<HTMLButtonElement>) => {\n        if (event.pointerType !== \"touch\") setRowHover(i);\n      },\n      onPointerLeave: () => setRowHover(null),\n      onFocus: (event: React.FocusEvent<HTMLButtonElement>) => {\n        // keyboard focus previews the line from `from`; focus that follows a click does not\n        if (event.currentTarget.matches(\":focus-visible\")) setFocusPreview(i);\n      },\n      onBlur: () => setFocusPreview(null),\n    };\n  }\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"geo-region-picker\"\n      data-disabled={disabled ? \"\" : undefined}\n      className={cn(\n        \"grid w-full max-w-[400px] gap-3 text-foreground [-webkit-tap-highlight-color:transparent] data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        hidden={layout === \"list\"}\n        className=\"rounded-xl border border-border bg-card p-2 shadow-[0_14px_34px_-20px_rgb(0_0_0/0.4)]\"\n      >\n        <div\n          ref={mapRef}\n          aria-hidden=\"true\"\n          className=\"relative w-full select-none\"\n          style={{ aspectRatio: MAP_ASPECT }}\n          onPointerMove={handleMapPointerMove}\n          onPointerLeave={() => setMapHover(null)}\n        >\n          <canvas ref={baseRef} className=\"pointer-events-none absolute inset-0 block size-full\" />\n          <canvas\n            ref={marksRef}\n            className=\"pointer-events-none absolute block\"\n            style={{\n              inset: -MARK_PAD,\n              width: `calc(100% + ${MARK_PAD * 2}px)`,\n              height: `calc(100% + ${MARK_PAD * 2}px)`,\n            }}\n          />\n          {list.map((region, i) => (\n            <button\n              key={region.id}\n              type=\"button\"\n              tabIndex={-1}\n              disabled={disabled}\n              onMouseDown={(event) => event.preventDefault()}\n              onClick={(event) => handleMarkerClick(event, i)}\n              className=\"absolute -mt-3 -ml-3 size-6 cursor-pointer rounded-full border-0 bg-transparent p-0 outline-none\"\n              style={{ left: percentX(points[i]), top: percentY(points[i]) }}\n            />\n          ))}\n          {fromPoint ? (\n            <span\n              className=\"pointer-events-none absolute rounded-[4px] bg-card/85 px-1 text-[10.5px] leading-[15px] font-medium whitespace-nowrap text-foreground\"\n              style={sideLabelStyle(fromPoint, fromSide)}\n            >\n              {fromLabel}\n            </span>\n          ) : null}\n          {hoverRegion && mapHover !== null ? (\n            <span\n              className=\"pointer-events-none absolute z-[1] flex items-center gap-1.5 rounded-md border border-border bg-card/95 px-1.5 py-0.5 text-[11.5px] leading-4 whitespace-nowrap shadow-[0_6px_16px_-10px_rgb(0_0_0/0.5)]\"\n              style={hoverLabelStyle(points[mapHover])}\n            >\n              <span className=\"font-medium text-foreground\">{hoverRegion.label}</span>\n              <span className=\"text-muted-foreground\">{hoverRegion.city}</span>\n              {hints ? (\n                <span className=\"font-mono text-[11px] text-muted-foreground tabular-nums\">\n                  {hintText(hints[mapHover])}\n                </span>\n              ) : null}\n            </span>\n          ) : null}\n          <div\n            ref={reticleRef}\n            data-hidden={selectedIndex < 0 ? \"\" : undefined}\n            className=\"pointer-events-none absolute z-[2] -mt-2.5 -ml-2.5 size-5 transition-opacity duration-150 will-change-transform data-[hidden]:opacity-0 motion-reduce:transition-none\"\n            style={{ left: percentX(reticleOrigin), top: percentY(reticleOrigin) }}\n          >\n            <div ref={bracketsRef} className=\"absolute inset-0\">\n              <span className=\"absolute top-0 left-0 size-[5px] border-t-[1.5px] border-l-[1.5px] border-lumesec\" />\n              <span className=\"absolute top-0 right-0 size-[5px] border-t-[1.5px] border-r-[1.5px] border-lumesec\" />\n              <span className=\"absolute bottom-0 left-0 size-[5px] border-b-[1.5px] border-l-[1.5px] border-lumesec\" />\n              <span className=\"absolute right-0 bottom-0 size-[5px] border-r-[1.5px] border-b-[1.5px] border-lumesec\" />\n            </div>\n            <span className=\"absolute top-1/2 left-1/2 -mt-[1.5px] -ml-[1.5px] size-[3px] bg-lumesec\" />\n          </div>\n          <canvas\n            ref={effectsRef}\n            className=\"pointer-events-none absolute -inset-x-3 -inset-y-8 z-[3] block h-[calc(100%+64px)] w-[calc(100%+24px)] min-[480px]:-inset-x-10 min-[480px]:w-[calc(100%+80px)]\"\n          />\n        </div>\n      </div>\n\n      <div className=\"rounded-xl border border-border bg-card shadow-[0_14px_34px_-20px_rgb(0_0_0/0.4)]\">\n        <div className=\"flex h-7 items-center justify-between gap-3 border-b border-border px-3 text-[12px] leading-4\">\n          <span id={labelId} className=\"shrink-0 font-medium text-foreground\">\n            {label}\n          </span>\n          {origin ? (\n            layout === \"map\" ? (\n              selected && hints ? (\n                <span aria-hidden=\"true\" className=\"flex min-w-0 items-center gap-1 text-muted-foreground\">\n                  <span className=\"font-mono text-foreground tabular-nums\">\n                    <LatencyHint ms={hints[selectedIndex]} />\n                  </span>\n                  <span className=\"truncate\">from {fromLabel}</span>\n                </span>\n              ) : null\n            ) : (\n              <span aria-hidden=\"true\" className=\"min-w-0 truncate text-muted-foreground\">\n                Round trip from {fromLabel}\n              </span>\n            )\n          ) : null}\n        </div>\n        <div\n          ref={groupRef}\n          role=\"radiogroup\"\n          aria-labelledby={labelId}\n          aria-required={required || undefined}\n          aria-disabled={disabled || undefined}\n          onKeyDown={handleGroupKeyDown}\n          className={cn(\n            \"relative\",\n            layout === \"map\"\n              ? \"flex flex-wrap gap-1.5 p-2\"\n              : \"grid max-h-[191px] [scrollbar-width:thin] overflow-y-auto p-1\",\n          )}\n        >\n          {list.length === 0 ? (\n            <p className=\"m-0 grid h-[68px] w-full place-items-center text-[12.5px] text-muted-foreground\">\n              No regions\n            </p>\n          ) : layout === \"map\" ? (\n            list.map((region, i) => (\n              <button\n                key={region.id}\n                {...radioProps(region, i)}\n                className=\"group/chip inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-md border border-border bg-card px-2 font-[inherit] text-[12px] leading-4 font-medium text-foreground transition-colors duration-150 outline-none hover:bg-muted/60 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-lumesec focus-visible:outline-solid data-[active]:bg-muted/60 data-[state=checked]:border-[color-mix(in_srgb,var(--lumesec)_55%,var(--border))] data-[state=checked]:bg-lumesec/8 motion-reduce:transition-none\"\n              >\n                <RadioDot />\n                {region.label}\n              </button>\n            ))\n          ) : (\n            list.map((region, i) => (\n              <button\n                key={region.id}\n                {...radioProps(region, i)}\n                className=\"group/chip flex h-[34px] w-full min-w-0 shrink-0 cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 text-left font-[inherit] transition-colors duration-150 outline-none hover:bg-muted/60 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-lumesec focus-visible:outline-solid data-[active]:bg-muted/60 motion-reduce:transition-none\"\n              >\n                <RadioDot />\n                <span className=\"shrink-0 text-[13px] leading-4 font-medium text-foreground\">{region.label}</span>\n                <span className=\"min-w-0 truncate text-[12.5px] leading-4 text-muted-foreground\">{region.city}</span>\n                {hints ? (\n                  <span className=\"ml-auto shrink-0 pl-2 text-right font-mono text-[12px] leading-4 text-muted-foreground tabular-nums group-data-[state=checked]/chip:text-foreground\">\n                    <LatencyHint ms={hints[i]} />\n                  </span>\n                ) : null}\n              </button>\n            ))\n          )}\n        </div>\n      </div>\n\n      {name && selected ? <input type=\"hidden\" name={name} value={selected.id} /> : null}\n    </div>\n  );\n}\n\n/* Parts ----------------------------------------------------------------------------------------------- */\n\n/** The 8 px radio square: a hairline frame, filled with the accent when checked. */\nfunction RadioDot() {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"size-2 shrink-0 rounded-[2px] border border-[color-mix(in_srgb,var(--foreground)_30%,var(--border))] transition-colors duration-150 group-data-[state=checked]/chip:border-lumesec group-data-[state=checked]/chip:bg-lumesec motion-reduce:transition-none\"\n    />\n  );\n}\n\n/** A round-trip hint as mechanical figures: it rolls up when the time grows and down when it shrinks. */\nfunction LatencyHint({ ms }: { ms: number }) {\n  const [shown, setShown] = React.useState({ ms, direction: 1 });\n  if (shown.ms !== ms) setShown({ ms, direction: ms >= shown.ms ? 1 : -1 });\n  return <RollText text={hintText(ms)} direction={shown.direction} className=\"justify-items-end\" />;\n}\n\n/* Helpers --------------------------------------------------------------------------------------------- */\n\ninterface Point {\n  readonly x: number;\n  readonly y: number;\n}\n\n/**\n * Regions with a finite position, the first of each id (ids are React keys and the form value). Returns the list\n * itself when nothing is skipped.\n */\nfunction usableRegions(regions: readonly GeoRegion[]): readonly GeoRegion[] {\n  const seen = new Set<string>();\n  const usable = regions.filter((region) => {\n    if (seen.has(region.id) || !Number.isFinite(region.lat) || !Number.isFinite(region.lon)) return false;\n    seen.add(region.id);\n    return true;\n  });\n  return usable.length === regions.length ? regions : usable;\n}\n\n/** Estimated round trip in whole ms over fibre, `stretch` times the great-circle distance. */\nfunction roundTripMs(start: LatLon, region: LatLon, stretch: number) {\n  return Math.round(fiberRttMs(distanceKm(start, region), stretch));\n}\n\nconst hintText = (ms: number) => (ms < 1 ? \"<1 ms\" : `~${formatNumber(ms)} ms`);\nconst spokenMs = (ms: number) => (ms < 1 ? \"under 1 ms\" : `${formatNumber(ms)} ms`);\n\n/** Position on the nominal map (1000 px wide), with the latitude held inside the map's crop. */\nfunction nominalPoint(place: LatLon): Point {\n  const point = createScreenPoint();\n  NOMINAL_PROJECTION.project(clamp(place.lat, LAT_RANGE[0], LAT_RANGE[1]), wrapLongitude(place.lon), point);\n  return { x: point.x, y: point.y };\n}\n\nconst percentX = (point: Point) => percent(point.x / NOMINAL_WIDTH);\nconst percentY = (point: Point) => percent(point.y / NOMINAL_HEIGHT);\n\ntype LabelSide = \"right\" | \"left\" | \"top\" | \"bottom\";\n\n/** Sides in order of preference. */\nconst LABEL_SIDES: readonly LabelSide[] = [\"right\", \"left\", \"top\", \"bottom\"];\n\n/** The `from` label beside its point, on the side `labelSide` chose. */\nfunction sideLabelStyle(point: Point, side: LabelSide): React.CSSProperties {\n  const transform =\n    side === \"right\"\n      ? `translate(${LABEL_GAP}px, -50%)`\n      : side === \"left\"\n        ? `translate(calc(-100% - ${LABEL_GAP}px), -50%)`\n        : side === \"top\"\n          ? `translate(-50%, calc(-100% - ${LABEL_GAP}px))`\n          : `translate(-50%, ${LABEL_GAP}px)`;\n  return { left: percentX(point), top: percentY(point), transform };\n}\n\n/**\n * The side of the `from` point whose label box covers the fewest region squares and stays on the map, so the label\n * never hides a region. The box is estimated in nominal units for a map about 380 px wide (the label is DOM text).\n */\nfunction labelSide(anchor: Point, text: string, points: readonly Point[]): LabelSide {\n  const scale = NOMINAL_WIDTH / LABEL_MAP_WIDTH;\n  const width = (text.length * LABEL_CHAR_WIDTH + 8) * scale;\n  const height = LABEL_HEIGHT * scale;\n  const gap = LABEL_GAP * scale;\n  const reach = (MARKER / 2 + 1) * scale;\n  let best: LabelSide = \"right\";\n  let fewest = Infinity;\n  for (const side of LABEL_SIDES) {\n    const x0 = side === \"right\" ? anchor.x + gap : side === \"left\" ? anchor.x - gap - width : anchor.x - width / 2;\n    const y0 = side === \"top\" ? anchor.y - gap - height : side === \"bottom\" ? anchor.y + gap : anchor.y - height / 2;\n    const x1 = x0 + width;\n    const y1 = y0 + height;\n    let score = x0 < 0 || x1 > NOMINAL_WIDTH || y0 < 0 || y1 > NOMINAL_HEIGHT ? 0.5 : 0;\n    for (const point of points) {\n      if (point.x > x0 - reach && point.x < x1 + reach && point.y > y0 - reach && point.y < y1 + reach) score += 1;\n    }\n    if (score < fewest) {\n      fewest = score;\n      best = side;\n    }\n  }\n  return best;\n}\n\n/** The hover label above the point (below it near the top edge), kept inside the map near the sides. */\nfunction hoverLabelStyle(point: Point): React.CSSProperties {\n  const fx = point.x / NOMINAL_WIDTH;\n  const below = point.y / NOMINAL_HEIGHT < 0.32;\n  const x = fx < 0.18 ? \"-10px\" : fx > 0.82 ? \"calc(-100% + 10px)\" : \"-50%\";\n  return {\n    left: percentX(point),\n    top: percentY(point),\n    transform: `translate(${x}, ${below ? \"10px\" : \"calc(-100% - 10px)\"})`,\n  };\n}\n\n/**\n * The nearest region west or east on the map (`spatialNavigate`). Past the last region that way it continues around\n * the globe: the other regions are shifted one map width over and searched again.\n */\nfunction neighbourOnMap(points: readonly Point[], index: number, direction: \"left\" | \"right\") {\n  const next = spatialNavigate(points, index, direction);\n  if (next !== index) return next;\n  const shift = direction === \"right\" ? NOMINAL_WIDTH : -NOMINAL_WIDTH;\n  const others: number[] = [];\n  points.forEach((_, i) => {\n    if (i !== index) others.push(i);\n  });\n  const wrapped = [points[index], ...others.map((i) => ({ x: points[i].x + shift, y: points[i].y }))];\n  const found = spatialNavigate(wrapped, 0, direction);\n  return found > 0 ? others[found - 1] : index;\n}\n\n/* Constants ------------------------------------------------------------------------------------------- */\n\nconst LAT_RANGE: readonly [number, number] = [-50, 72];\nconst MAP_ASPECT = projectionAspect(\"equalEarth\", LAT_RANGE);\nconst NOMINAL_WIDTH = 1000;\nconst NOMINAL_HEIGHT = NOMINAL_WIDTH / MAP_ASPECT;\nconst NOMINAL_PROJECTION = createProjection(\"equalEarth\", {\n  width: NOMINAL_WIDTH,\n  height: NOMINAL_HEIGHT,\n  latRange: LAT_RANGE,\n});\nconst MAP_MIDDLE: Point = { x: NOMINAL_WIDTH / 2, y: NOMINAL_HEIGHT / 2 };\nconst MAP_CANVAS = { maxDpr: 2 } as const;\n/** The marks canvas reaches this far past the map (the map card's padding), so the lens is not cut at the edge. */\nconst MARK_PAD = 8;\n/** Pointer reach of a map region in CSS px (the 24 px targets). */\nconst HIT_RADIUS = 12;\nconst SCROLL_MARGIN = 4;\n\n/** Lattice pitch, dot and marker sizes in CSS px. */\nconst PITCH = 3.5;\nconst DOT = 1.5;\nconst MARKER = 7;\nconst FROM_POINT = 4;\n\n/** The `from` label: its gap to the point and the estimates used to choose its side. */\nconst LABEL_GAP = 6;\nconst LABEL_HEIGHT = 15;\nconst LABEL_CHAR_WIDTH = 5.8;\nconst LABEL_MAP_WIDTH = 380;\n\n/** The lens: a 28 px disc; dots are pushed out by r' = r·(1 + 0.55·k·(1 − r/28)²) and drawn up to 1.3 times larger. */\nconst LENS_RADIUS = 28;\nconst LENS_PUSH = 0.55;\nconst LENS_GROW = 0.3;\n/** Lens strength at rest; it reaches 1 at this travel speed in px/s. */\nconst LENS_REST = 0.6;\nconst LENS_FULL_SPEED = 240;\n\n/** The great-circle path is sampled at 64 segments. */\nconst TRAVEL_SEGMENTS = 64;\nconst TRAVEL_DAMPING = 0.9;\n/** Progress at which the reticle counts as arrived: the brackets snap and the sparks fly. */\nconst ARRIVAL = 0.985;\n/** A retarget carries the reticle's speed into the new path, up to this many paths per second. */\nconst MAX_CARRY = 4;\n\n/** The comet: 14 squares behind the reticle, spaced by speed, 2 → 1 px and alpha 0.8 → 0. */\nconst COMET_LENGTH = 14;\nconst COMET_SPACING_MAX = 4.5;\nconst COMET_SPACING_PER_SPEED = 0.012;\nconst COMET_FADE: readonly [number, number] = [24, 160];\n\n/** The line from `from`: drawn on over 250 ms, faded over 180 ms. */\nconst ARC_MS = 250;\nconst ARC_FADE_MS = 180;\nconst ARC_SPACING = 3.5;\nconst ARC_SEGMENTS = 48;\nconst ARC_EASE = cubicBezier(0.45, 0, 0.25, 1);\n\nconst SNAP_KEYFRAMES: Keyframe[] = [{ transform: \"scale(1.4)\" }, { transform: \"scale(1)\" }];\nconst SNAP_TIMING: KeyframeAnimationOptions = { duration: 260, easing: \"cubic-bezier(.2,1.4,.4,1)\" };\nconst ARRIVAL_SPARKS: SparkOptions = { speed: [50, 170], life: [0.3, 0.7], size: [1.5, 2], jitter: 1.5 };\n\n/** Spring stiffness of a trip: stiffer for short hops, softer for long ones, so long trips take longer. */\nconst travelStiffness = (km: number) => 180 * Math.sqrt(5000 / Math.max(km, 500));\n\n/* Map engine ------------------------------------------------------------------------------------------ */\n\ninterface Place {\n  /** Latitude held inside the map's crop. */\n  readonly lat: number;\n  readonly lon: number;\n  /** Position on the map in CSS px, updated on resize. */\n  x: number;\n  y: number;\n}\n\n/** One trip of the reticle: the sampled great circle and its projected arc-length table. */\ninterface Travel {\n  readonly lats: Float64Array;\n  /** Longitudes east of the central meridian, unwrapped, so the path is continuous across the seam. */\n  readonly lambdas: Float64Array;\n  readonly xs: Float32Array;\n  readonly ys: Float32Array;\n  readonly table: ArcLengthTable;\n  readonly km: number;\n  readonly stiffness: number;\n  arrived: boolean;\n}\n\ninterface Arc {\n  readonly index: number;\n  readonly tables: ArcLengthTable[];\n  readonly start: number;\n  /** When the fade started, or −1 while shown. */\n  fade: number;\n}\n\n/**\n * Everything the map draws, outside React. Three canvases: the land lattice (baked), the marks (the lens, the line\n * from `from`, the region squares and the comet; it reaches 8 px past the map) and a larger effects layer for the\n * arrival sparks, which may fly past the map's edge. The reticle itself is DOM; the component moves it to `reticle`.\n */\nclass RegionMap {\n  /** Called on the frame the reticle arrives at the selected region. */\n  onArrive: () => void = () => undefined;\n\n  private ink: GeoInk | null = null;\n  private styles = { land: \"\", landStrong: \"\", card: \"\", accent: \"\", glint: \"\", preview: \"\" };\n  private box: { width: number; height: number; dpr: number } | null = null;\n  private projection: MapProjection | null = null;\n  private outline: Path2D | null = null;\n  private lattice: DotLattice | null = null;\n  private regionsKey: string | null = null;\n  private places: Place[] = [];\n  private selected = -1;\n  private preview = -1;\n  private fromKey = \"\";\n  private from: Place | null = null;\n  private travel: Travel | null = null;\n  private readonly progress = { position: 1, velocity: 0 };\n  private readonly point = { lat: 0, lon: 0, x: 0, y: 0, shown: false };\n  /** Travel speed in px/s; it drives the comet's spacing and the lens strength. */\n  private speed = 0;\n  private lens = LENS_REST;\n  private arcs: Arc[] = [];\n  private readonly sparks = new Sparks();\n  private effectsX = 0;\n  private effectsY = 0;\n  private dirty = true;\n  private baseDirty = true;\n  private effectsDirty = false;\n  private wasMoving = false;\n  private readonly scratch = createScreenPoint();\n  private readonly sample = { lat: 0, lambda: 0 };\n\n  /** The map's size in CSS px, once laid out. */\n  get size(): { readonly width: number; readonly height: number } | null {\n    return this.box;\n  }\n\n  /** Where the reticle is, in map CSS px, and whether it shows. */\n  get reticle(): { readonly x: number; readonly y: number; readonly shown: boolean } {\n    return this.point;\n  }\n\n  /** New theme colours: the baked land and the marks follow. */\n  setInk(ink: GeoInk) {\n    this.ink = ink;\n    this.styles = {\n      land: rgba(ink.land),\n      landStrong: rgba(ink.landStrong),\n      card: rgba(ink.card),\n      accent: rgba(ink.accent),\n      glint: rgba(ink.glint),\n      preview: rgba(mixRgb(ink.landStrong, ink.accent, 0.55)),\n    };\n    this.baseDirty = true;\n    this.dirty = true;\n  }\n\n  /** New map size: projection, lattice, places, the trip and the lines follow. */\n  layout(width: number, height: number, dpr: number) {\n    this.box = { width, height, dpr };\n    const projection = createProjection(\"equalEarth\", { width, height, latRange: LAT_RANGE });\n    this.projection = projection;\n    this.outline = new Path2D(projectionOutlineSvgPath(projection));\n    this.lattice = mapLattice(projection, { pitch: PITCH });\n    for (const place of this.places) this.place(place);\n    if (this.from) this.place(this.from);\n    if (this.travel) this.projectTravel(this.travel);\n    this.arcs = this.arcs.flatMap((arc) => {\n      if (arc.fade >= 0) return [];\n      const next = this.arcTo(arc.index, arc.start);\n      return next ? [next] : [];\n    });\n    this.baseDirty = true;\n    this.dirty = true;\n  }\n\n  /** The marks canvas was resized (and cleared). */\n  invalidate() {\n    this.dirty = true;\n  }\n\n  /** Position of the map's top-left corner on the effects layer. */\n  setEffectsOffset(x: number, y: number) {\n    this.effectsX = x;\n    this.effectsY = y;\n  }\n\n  /** The regions to show. A different set drops any trip and puts the reticle on `selected` at once. */\n  setRegions(regions: readonly LatLon[], selected: number) {\n    const key = regions.map((region) => `${region.lat},${region.lon}`).join(\";\");\n    if (key === this.regionsKey) return;\n    this.regionsKey = key;\n    this.places = regions.map((region) => {\n      const place: Place = {\n        lat: clamp(region.lat, LAT_RANGE[0], LAT_RANGE[1]),\n        lon: wrapLongitude(region.lon),\n        x: 0,\n        y: 0,\n      };\n      this.place(place);\n      return place;\n    });\n    this.selected = selected < this.places.length ? selected : -1;\n    this.jumpTo(this.selected);\n    const preview = this.preview;\n    this.preview = -1;\n    this.arcs = [];\n    this.setPreview(preview < this.places.length ? preview : -1);\n    this.dirty = true;\n  }\n\n  /**\n   * Selects a region. With `animate`, the reticle travels there along the great circle from wherever it is now (a\n   * change during travel retargets from the current point); otherwise it jumps.\n   */\n  setSelected(index: number, animate: boolean) {\n    if (index === this.selected) return;\n    this.selected = index;\n    this.dirty = true;\n    const target = this.places[index];\n    const projection = this.projection;\n    if (!target || !animate || !projection || !this.point.shown) {\n      this.jumpTo(index);\n      return;\n    }\n    const start: LatLon = { lat: this.point.lat, lon: this.point.lon };\n    const km = distanceKm(start, target);\n    if (!(km >= 1)) {\n      this.jumpTo(index);\n      return;\n    }\n    const carried = this.travel ? Math.abs(this.progress.velocity) * this.travel.table.length : 0;\n    const travel = createTravel(start, target, km, projection.centerLon);\n    this.projectTravel(travel);\n    this.travel = travel;\n    this.progress.position = 0;\n    this.progress.velocity = travel.table.length > 0 ? Math.min(carried / travel.table.length, MAX_CARRY) : 0;\n  }\n\n  /** The location the hints are measured from, or null. */\n  setFrom(from: LatLon | null) {\n    const key = from ? `${from.lat},${from.lon}` : \"\";\n    if (key === this.fromKey) return;\n    this.fromKey = key;\n    this.from = from\n      ? { lat: clamp(from.lat, LAT_RANGE[0], LAT_RANGE[1]), lon: wrapLongitude(from.lon), x: 0, y: 0 }\n      : null;\n    if (this.from) this.place(this.from);\n    const preview = this.preview;\n    this.preview = -1;\n    this.arcs = [];\n    this.setPreview(preview);\n    this.dirty = true;\n  }\n\n  /** The region the pointer (or keyboard focus) is on: a dotted line draws to it from `from`. −1 for none. */\n  setPreview(index: number) {\n    if (index === this.preview) return;\n    const now = performance.now();\n    const reduced = prefersReducedMotion();\n    for (const arc of this.arcs) if (arc.fade < 0) arc.fade = reduced ? now - ARC_FADE_MS : now;\n    this.preview = index;\n    this.dirty = true;\n    const arc = this.arcTo(index, reduced ? now - ARC_MS : now);\n    if (arc) this.arcs.push(arc);\n  }\n\n  /** One frame. Returns whether to keep going. */\n  step(base: CanvasSurface | null, marks: CanvasSurface | null, effects: CanvasSurface | null, delta: number) {\n    const ink = this.ink;\n    const projection = this.projection;\n    if (!ink || !this.box || !projection || !this.lattice || !base || !marks) return false;\n    const now = performance.now();\n    const reduced = prefersReducedMotion();\n    let moving = false;\n\n    if (this.baseDirty) {\n      this.bake(base);\n      this.baseDirty = false;\n    }\n\n    // The trip: progress springs to 1, and the reticle follows the path at that share of its length.\n    this.speed = 0;\n    const travel = this.travel;\n    if (travel) {\n      if (reduced) {\n        this.progress.position = 1;\n        this.progress.velocity = 0;\n      } else if (delta > 0) {\n        springStep(this.progress, 1, delta, travel.stiffness, TRAVEL_DAMPING);\n      }\n      const length = travel.table.length;\n      this.speed = Math.abs(this.progress.velocity) * length;\n      if (!travel.arrived && this.progress.position >= ARRIVAL) {\n        travel.arrived = true;\n        if (!reduced) this.arrive(travel);\n      }\n      const resting = Math.abs(1 - this.progress.position) * length < 0.25 && this.speed < 2;\n      if (reduced || resting) {\n        this.jumpTo(this.selected);\n        this.speed = 0;\n      } else {\n        this.pointOnTravel(travel, clamp(this.progress.position, 0, 1) * length);\n        this.point.x = this.scratch.x;\n        this.point.y = this.scratch.y;\n        this.point.lat = this.sample.lat;\n        this.point.lon = wrapLongitude(this.sample.lambda + projection.centerLon);\n        moving = true;\n      }\n    }\n    if (!this.travel && this.point.shown) {\n      const place = this.places[this.selected];\n      if (place) {\n        this.point.x = place.x;\n        this.point.y = place.y;\n      }\n    }\n\n    // The lens follows the speed: full strength in flight, 0.6 at rest.\n    const lens = reduced ? LENS_REST : LENS_REST + (1 - LENS_REST) * clamp(this.speed / LENS_FULL_SPEED, 0, 1);\n    if (Math.abs(lens - this.lens) > 0.001) {\n      this.lens = lens;\n      this.dirty = true;\n    }\n\n    if (this.arcs.length) {\n      this.arcs = this.arcs.filter((arc) => arc.fade < 0 || now - arc.fade < ARC_FADE_MS);\n      if (this.arcs.some((arc) => arc.fade >= 0 || now - arc.start < ARC_MS)) moving = true;\n    }\n\n    // one more frame after the last movement, so everything rests at its final state\n    if (this.dirty || moving || this.wasMoving) {\n      this.drawMarks(marks, ink, now, reduced);\n      this.dirty = false;\n    }\n    this.wasMoving = moving;\n    const sparking = effects ? this.drawEffects(effects, delta) : false;\n    return moving || sparking;\n  }\n\n  /* internals */\n\n  private place(place: Place) {\n    if (!this.projection) return;\n    this.projection.project(place.lat, place.lon, this.scratch);\n    place.x = this.scratch.x;\n    place.y = this.scratch.y;\n  }\n\n  /** Ends any trip and puts the reticle on region `index` (hidden for −1). */\n  private jumpTo(index: number) {\n    this.travel = null;\n    this.progress.position = 1;\n    this.progress.velocity = 0;\n    const place = this.places[index];\n    if (!place) {\n      this.point.shown = false;\n      return;\n    }\n    this.point.lat = place.lat;\n    this.point.lon = place.lon;\n    this.point.x = place.x;\n    this.point.y = place.y;\n    this.point.shown = true;\n  }\n\n  /** Projects the trip's samples (unwrapped, so the path runs on past the seam) and measures it. */\n  private projectTravel(travel: Travel) {\n    const projection = this.projection;\n    if (!projection) return;\n    for (let i = 0; i < travel.lats.length; i++) {\n      projection.projectRelative(travel.lats[i], travel.lambdas[i], this.scratch);\n      travel.xs[i] = this.scratch.x;\n      travel.ys[i] = this.scratch.y;\n    }\n    buildArcLength(travel.xs, travel.ys, travel.lats.length, travel.table);\n  }\n\n  /**\n   * The point `s` px along the trip, into `sample` (lat/lon) and `scratch` (map px). The longitude is wrapped back\n   * into the map, so a trip across the seam leaves one edge and comes back in at the other.\n   */\n  private pointOnTravel(travel: Travel, s: number) {\n    const { table, lats, lambdas } = travel;\n    const d = clamp(s, 0, table.length);\n    let low = 0;\n    let high = table.count - 2;\n    while (low < high) {\n      const middle = (low + high + 1) >> 1;\n      if (table.distances[middle] <= d) low = middle;\n      else high = middle - 1;\n    }\n    const span = table.distances[low + 1] - table.distances[low];\n    const t = span > 0 ? clamp((d - table.distances[low]) / span, 0, 1) : 0;\n    this.sample.lat = lats[low] + (lats[low + 1] - lats[low]) * t;\n    this.sample.lambda = lambdas[low] + (lambdas[low + 1] - lambdas[low]) * t;\n    this.projection?.projectRelative(this.sample.lat, wrapLongitude(this.sample.lambda), this.scratch);\n  }\n\n  /** A dotted great-circle line from `from` to region `index`, or null when there is nothing to draw. */\n  private arcTo(index: number, start: number): Arc | null {\n    const from = this.from;\n    const place = this.places[index];\n    const projection = this.projection;\n    if (!from || !place || !projection) return null;\n    if (Math.hypot(place.x - from.x, place.y - from.y) < 6) return null;\n    return {\n      index,\n      tables: flatPaths(projection, from, place, { geodesic: true, segments: ARC_SEGMENTS }),\n      start,\n      fade: -1,\n    };\n  }\n\n  /** The reticle reached the region: the component snaps the brackets; sparks fly, more for longer trips. */\n  private arrive(travel: Travel) {\n    const ink = this.ink;\n    const place = this.places[this.selected];\n    if (!ink || !place) return;\n    this.onArrive();\n    const count = Math.round(6 + 8 * clamp(travel.km / 8000, 0, 1));\n    this.sparks.add(place.x + this.effectsX, place.y + this.effectsY, count, [ink.accent, ink.glint], ARRIVAL_SPARKS);\n  }\n\n  private bake(surface: CanvasSurface) {\n    const { context, width, height, dpr } = surface;\n    const lattice = this.lattice;\n    context.clearRect(0, 0, width, height);\n    if (!lattice) return;\n    context.fillStyle = this.styles.land;\n    for (let i = 0; i < lattice.count; i++) fillSquare(context, lattice.x[i], lattice.y[i], DOT, dpr);\n  }\n\n  private drawMarks(surface: CanvasSurface, ink: GeoInk, now: number, reduced: boolean) {\n    const { context, width, height, dpr } = surface;\n    context.clearRect(0, 0, width, height);\n    context.save();\n    context.translate(MARK_PAD, MARK_PAD);\n    if (this.point.shown) this.drawLens(context, ink, dpr);\n    if (this.arcs.length && this.outline) {\n      context.save();\n      context.clip(this.outline);\n      for (const arc of this.arcs) this.drawArc(context, arc, now, dpr);\n      context.restore();\n    }\n    this.drawPlaces(context, dpr);\n    if (this.from) {\n      context.fillStyle = this.styles.card;\n      fillSquare(context, this.from.x, this.from.y, FROM_POINT + 2, dpr);\n      context.fillStyle = this.styles.accent;\n      fillSquare(context, this.from.x, this.from.y, FROM_POINT, dpr);\n    }\n    if (this.travel && !reduced) this.drawComet(context, this.travel, dpr);\n    context.restore();\n  }\n\n  /**\n   * The lens: a card-coloured disc over the baked dots, and the lattice dots inside it pushed outward, enlarged and\n   * tinted toward the accent, most at the centre.\n   */\n  private drawLens(context: CanvasRenderingContext2D, ink: GeoInk, dpr: number) {\n    const lattice = this.lattice;\n    if (!lattice) return;\n    const { x: cx, y: cy } = this.point;\n    const strength = this.lens;\n    context.save();\n    context.beginPath();\n    context.arc(cx, cy, LENS_RADIUS, 0, TAU);\n    context.fillStyle = this.styles.card;\n    context.fill();\n    context.clip();\n    lattice.grid.query(cx, cy, LENS_RADIUS, (i, distanceSquared) => {\n      const r = Math.sqrt(distanceSquared);\n      if (r >= LENS_RADIUS) return;\n      const u = 1 - r / LENS_RADIUS;\n      const push = 1 + LENS_PUSH * strength * u * u;\n      // the growth fades out toward the rim, so the lens has no hard edge against the lattice outside\n      const size = DOT * (1 + LENS_GROW * strength * Math.min(1, u * 3));\n      context.fillStyle = rgba(mixRgb(ink.land, ink.accent, clamp(strength * u, 0, 1)));\n      fillSquare(context, cx + (lattice.x[i] - cx) * push, cy + (lattice.y[i] - cy) * push, size, dpr);\n    });\n    context.restore();\n  }\n\n  /** The line from `from`: dotted, brighter toward the region, with a glint head while it draws on. */\n  private drawArc(context: CanvasRenderingContext2D, arc: Arc, now: number, dpr: number) {\n    const progress = ARC_EASE(clamp((now - arc.start) / ARC_MS, 0, 1));\n    const fade = arc.fade < 0 ? 1 : 1 - clamp((now - arc.fade) / ARC_FADE_MS, 0, 1);\n    if (fade <= 0) return;\n    for (const table of arc.tables) {\n      const drawn = table.length * progress;\n      const length = Math.max(1, table.length);\n      forEachDot(table, ARC_SPACING, 0, drawn, (x, y, s) => {\n        const head = progress < 1 ? Math.exp(-(drawn - s) / 10) : 0;\n        context.globalAlpha = fade * clamp(0.4 + 0.45 * (s / length) + 0.4 * head, 0, 1);\n        context.fillStyle = head > 0.5 ? this.styles.glint : this.styles.accent;\n        fillSquare(context, x, y, DOT + 0.5 * head, dpr);\n      });\n    }\n    context.globalAlpha = 1;\n  }\n\n  /** Region squares with a 1 px card ring: quiet ones first, then the pointed one, then the selected one on top. */\n  private drawPlaces(context: CanvasRenderingContext2D, dpr: number) {\n    for (let pass = 0; pass < 3; pass++) {\n      for (let i = 0; i < this.places.length; i++) {\n        const rank = i === this.selected ? 2 : i === this.preview ? 1 : 0;\n        if (rank !== pass) continue;\n        const place = this.places[i];\n        const size = MARKER + (rank > 0 ? 1 : 0);\n        context.fillStyle = this.styles.card;\n        fillSquare(context, place.x, place.y, size + 2, dpr);\n        context.fillStyle = rank === 2 ? this.styles.accent : rank === 1 ? this.styles.preview : this.styles.landStrong;\n        fillSquare(context, place.x, place.y, size, dpr);\n      }\n    }\n  }\n\n  /** The comet behind the reticle: spaced by speed, so it stretches in flight and folds into the reticle on arrival. */\n  private drawComet(context: CanvasRenderingContext2D, travel: Travel, dpr: number) {\n    const spacing = Math.min(COMET_SPACING_MAX, this.speed * COMET_SPACING_PER_SPEED);\n    const fade = smoothstep(COMET_FADE[0], COMET_FADE[1], this.speed);\n    if (spacing < 0.2 || fade <= 0) return;\n    const head = clamp(this.progress.position, 0, 1) * travel.table.length;\n    for (let i = 1; i <= COMET_LENGTH; i++) {\n      const s = head - i * spacing;\n      if (s < 0) break;\n      this.pointOnTravel(travel, s);\n      const f = (i - 1) / (COMET_LENGTH - 1);\n      context.globalAlpha = 0.8 * (1 - f) * fade;\n      context.fillStyle = i === 1 ? this.styles.glint : this.styles.accent;\n      fillSquare(context, this.scratch.x, this.scratch.y, 2 - f, dpr);\n    }\n    context.globalAlpha = 1;\n  }\n\n  private drawEffects(surface: CanvasSurface, delta: number) {\n    const { context, width, height } = surface;\n    if (!this.sparks.active) {\n      if (this.effectsDirty) {\n        context.clearRect(0, 0, width, height);\n        this.effectsDirty = false;\n      }\n      return false;\n    }\n    context.clearRect(0, 0, width, height);\n    this.effectsDirty = true;\n    return this.sparks.step(context, delta);\n  }\n}\n\n/** Samples the great circle from `start` to `target` (latitudes held inside the crop, longitudes unwrapped). */\nfunction createTravel(start: LatLon, target: LatLon, km: number, centerLon: number): Travel {\n  const along = interpolateGreatCircle(start, target);\n  const count = TRAVEL_SEGMENTS + 1;\n  const lats = new Float64Array(count);\n  const lambdas = new Float64Array(count);\n  for (let i = 0; i < count; i++) {\n    const point = i === 0 ? start : i === count - 1 ? target : along(i / TRAVEL_SEGMENTS);\n    lats[i] = clamp(point.lat, LAT_RANGE[0], LAT_RANGE[1]);\n    const lambda = point.lon - centerLon;\n    lambdas[i] = i === 0 ? wrapLongitude(lambda) : unwrapLongitude(lambdas[i - 1], lambda);\n  }\n  const xs = new Float32Array(count);\n  const ys = new Float32Array(count);\n  return {\n    lats,\n    lambdas,\n    xs,\n    ys,\n    table: buildArcLength(xs, ys, count),\n    km,\n    stiffness: travelStiffness(km),\n    arrived: false,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/lumesec/geo-region-picker.tsx"
    }
  ],
  "meta": {
    "tag": "geo-region-picker",
    "collection": "location",
    "react": "GeoRegionPicker",
    "kind": "react",
    "data": "input"
  },
  "docs": "Region picker docs: https://elements.lumesec.ai/components/location/region-picker",
  "categories": [
    "location",
    "inputs-time"
  ],
  "type": "registry:component"
}