{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shape-map",
  "type": "registry:ui",
  "title": "Shape Map",
  "description": "남한 행정구역을 SVG 도형으로 그리고 지역별 상태를 색으로 보여주는 지도입니다. 시·도에서 시·군·구로 들어갑니다.",
  "registryDependencies": [
    "@atom/tokens",
    "@atom/lib-bem",
    "@atom/lib-cn",
    "@atom/lib-use-controllable-state",
    "@atom/shape-map-data"
  ],
  "files": [
    {
      "path": "poc/design-system/implement/src/components/shape-map.tsx",
      "content": "\"use client\";\n\nimport { type HTMLAttributes, type KeyboardEvent, type Ref, useMemo, useState } from \"react\";\nimport { mod } from \"../lib/bem\";\nimport { cn } from \"../lib/cn\";\nimport { useControllableState } from \"../lib/use-controllable-state\";\nimport { shapeMapGuAreas, shapeMapGuViewBox, shapeMapSidoRegions, shapeMapSidoViewBox } from \"./shape-map-data\";\n\n/* -------------------------------------------------------------------------- */\n/*  Shape Map (components/shape-map.md, styles/atom/elements/shape-map.css)   */\n/*    const [code, setCode] = useState<string | null>(null);                  */\n/*    <ShapeMap                                                               */\n/*      level={code ? \"gu\" : \"sido\"}                                          */\n/*      regionCode={code ?? undefined}                                        */\n/*      states={code ? { 강남구: \"critical\" } : { 서울: \"critical\", 경기: \"warning\" }} */\n/*      counts={{ 서울: 12 }}                                                  */\n/*      onRegionSelect={setCode}                                              */\n/*      onBack={() => setCode(null)}                                          */\n/*    />                                                                      */\n/*  지오메트리는 ./shape-map-data (원본 데모 마크업에서 추출). 지역 키는 지역명입니다 */\n/*  (시·도: \"서울\" · \"경기\" …, 시·군·구: \"강남구\" …).                              */\n/* -------------------------------------------------------------------------- */\n\nexport type ShapeMapLevel = \"sido\" | \"gu\";\nexport type ShapeMapStatus = \"normal\" | \"warning\" | \"critical\";\nexport type ShapeMapDetail = \"none\" | \"district\";\nexport type ShapeMapView = \"map\" | \"table\";\n\nexport type ShapeMapProps = Omit<HTMLAttributes<HTMLDivElement>, \"children\"> & {\n  ref?: Ref<HTMLDivElement>;\n  /** 어느 레벨을 그릴지입니다. */\n  level?: ShapeMapLevel;\n  /** `level: \"gu\"` 일 때 어느 시·도의 구를 그릴지입니다. (지오메트리가 있는 시·도: \"서울\") */\n  regionCode?: string;\n  /** 지역 코드별 상태입니다. 비어 있으면 감시 대상이 없는 빈 상태입니다. */\n  states: Record<string, ShapeMapStatus>;\n  /** 툴팁과 표에 쓰는 건수입니다. */\n  counts?: Record<string, number>;\n  /** 시·도 레벨에서 구 경계를 함께 그릴지입니다. */\n  detail?: ShapeMapDetail;\n  /** 지도 위에 지역명을 얹습니다. */\n  showLabels?: boolean;\n  /** 어느 쌍둥이를 보일지입니다(controlled). */\n  view?: ShapeMapView;\n  /** uncontrolled 일 때 처음 보일 뷰 (구현 추가) */\n  defaultView?: ShapeMapView;\n  /** 헤더의 지도/표 전환 칩으로 뷰가 바뀔 때 (구현 추가) */\n  onViewChange?: (view: ShapeMapView) => void;\n  /** 헤더에 지도/표 전환 칩(atom-shape-map__toggle)을 둡니다 (구현 추가) */\n  viewToggle?: boolean;\n  /** 감시 대상이 없을 때 문구입니다. */\n  emptyText?: string;\n  /** 지역을 누르거나 `Enter` 했을 때. 드릴다운을 여기서 처리합니다. */\n  onRegionSelect?: (code: string) => void;\n  /** 전국으로 돌아갈 때. */\n  onBack?: () => void;\n};\n\nconst B = \"atom-shape-map\";\n\nconst STATUS_WORD: Record<ShapeMapStatus, string> = { normal: \"정상\", warning: \"주의\", critical: \"장애\" };\nconst LEGEND: ShapeMapStatus[] = [\"normal\", \"warning\", \"critical\"];\n\n/** 좁은 시·도(특별·광역시)는 지도 위 라벨을 생략하고 툴팁과 표에 맡깁니다 — viewBox 단위 폭 */\nconst LABEL_MIN_WIDTH = 36;\n\ntype Shape = { code: string; paths: readonly string[]; cx: number; cy: number; width: number };\n\n/** path d 의 좌표로 바운딩 박스 중심을 구합니다 (툴팁·라벨 위치). DOM 측정 없이 데이터만으로. */\nfunction measure(paths: readonly string[]) {\n  let x0 = Infinity;\n  let y0 = Infinity;\n  let x1 = -Infinity;\n  let y1 = -Infinity;\n  for (const d of paths) {\n    const nums = d.match(/-?\\d+(?:\\.\\d+)?/g) ?? [];\n    for (let i = 0; i + 1 < nums.length; i += 2) {\n      const x = Number(nums[i]);\n      const y = Number(nums[i + 1]);\n      if (x < x0) x0 = x;\n      if (x > x1) x1 = x;\n      if (y < y0) y0 = y;\n      if (y > y1) y1 = y;\n    }\n  }\n  return { cx: (x0 + x1) / 2, cy: (y0 + y1) / 2, width: x1 - x0 };\n}\n\nconst statusAttr = (s: ShapeMapStatus | undefined) => (s && s !== \"normal\" ? s : undefined);\n\nfunction ShapeMapRoot({\n  ref,\n  level = \"sido\",\n  regionCode,\n  states,\n  counts = {},\n  detail = \"none\",\n  showLabels = false,\n  view: viewProp,\n  defaultView = \"map\",\n  onViewChange,\n  viewToggle = false,\n  emptyText = \"감시 대상이 없습니다\",\n  onRegionSelect,\n  onBack,\n  className,\n  onKeyDown,\n  ...rest\n}: ShapeMapProps) {\n  const [view, setView] = useControllableState<ShapeMapView>({\n    value: viewProp,\n    defaultValue: defaultView,\n    onChange: onViewChange,\n  });\n  const [active, setActive] = useState<string | null>(null);\n\n  const geo = useMemo(() => {\n    if (level === \"gu\") {\n      const areas = regionCode ? shapeMapGuAreas[regionCode] : undefined;\n      const viewBox = regionCode ? shapeMapGuViewBox[regionCode] : undefined;\n      if (!areas || !viewBox) return null;\n      return { viewBox, shapes: areas.map((a): Shape => ({ code: a.area, paths: [a.d], ...measure([a.d]) })) };\n    }\n    return {\n      viewBox: shapeMapSidoViewBox,\n      shapes: shapeMapSidoRegions.map((r): Shape => ({ code: r.region, paths: r.paths, ...measure(r.paths) })),\n    };\n  }, [level, regionCode]);\n\n  const isEmpty = !geo || Object.keys(states).length === 0;\n  const [, , vbW = 1, vbH = 1] = (geo?.viewBox ?? \"0 0 1 1\").split(\" \").map(Number);\n  const caption = level === \"gu\" ? `${regionCode ?? \"\"} · 시·군·구` : \"전국 · 시·도\";\n  const mapLabel = level === \"gu\" ? `${regionCode ?? \"\"} 시·군·구 장애 감시 지도` : \"전국 시·도 장애 감시 지도\";\n\n  // Esc 가 전국으로 돌아옵니다\n  const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(e);\n    if (e.defaultPrevented || e.key !== \"Escape\") return;\n    if (active) setActive(null);\n    if (level === \"gu\" && onBack) {\n      e.preventDefault();\n      onBack();\n    }\n  };\n\n  if (isEmpty) {\n    // 감시 대상이 없으면 회색 지도를 그리지 않습니다 — 전부 정상인 것과 다른 상태\n    return (\n      <div {...rest} ref={ref} className={cn(B, mod(B, \"detail\", detail, \"none\"), className)} data-level={level} data-view={view}>\n        <p className={`${B}__empty`}>{emptyText}</p>\n      </div>\n    );\n  }\n\n  const shapeProps = (shape: Shape) => {\n    const status = states[shape.code];\n    return {\n      \"data-status\": statusAttr(status),\n      tabIndex: 0,\n      role: \"button\",\n      // 이름에 지역명과 상태를 함께 — 색 없이도 상태가 읽히게\n      \"aria-label\": status ? `${shape.code}, ${STATUS_WORD[status]}` : shape.code,\n      onClick: () => onRegionSelect?.(shape.code),\n      onKeyDown: (e: KeyboardEvent<SVGElement>) => {\n        if (e.key === \"Enter\" || e.key === \" \") {\n          e.preventDefault();\n          onRegionSelect?.(shape.code);\n        }\n      },\n      onPointerEnter: () => setActive(shape.code),\n      onPointerLeave: () => setActive((cur) => (cur === shape.code ? null : cur)),\n      onFocus: () => setActive(shape.code),\n      onBlur: () => setActive((cur) => (cur === shape.code ? null : cur)),\n    };\n  };\n\n  const activeShape = active ? geo.shapes.find((s) => s.code === active) : undefined;\n  const activeStatus = activeShape ? (states[activeShape.code] ?? \"normal\") : \"normal\";\n\n  return (\n    <div\n      {...rest}\n      ref={ref}\n      className={cn(B, mod(B, \"detail\", detail, \"none\"), className)}\n      data-level={level}\n      data-view={view}\n      onKeyDown={handleKeyDown}\n    >\n      <div className={`${B}__header`}>\n        {level === \"gu\" ? (\n          <button className={`${B}__back`} type=\"button\" onClick={() => onBack?.()}>\n            <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n              <path d=\"M9 5l7 7-7 7\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n            </svg>\n            <span>전국으로</span>\n          </button>\n        ) : (\n          <span style={{ fontSize: 13, color: \"var(--atom-fg-neutral-muted)\" }}>{caption}</span>\n        )}\n        {viewToggle && (\n          <span className={`${B}__toggle`}>\n            {([\"map\", \"table\"] as const).map((v) => {\n              const selected = view === v;\n              return (\n                <button\n                  key={v}\n                  type=\"button\"\n                  className={cn(\"atom-chip\", \"atom-chip--size_small\", selected && \"atom-chip--variant_neutral-solid\")}\n                  data-selected={selected ? \"\" : undefined}\n                  aria-pressed={selected}\n                  onClick={() => setView(v)}\n                >\n                  <span className=\"atom-chip__label\">{v === \"map\" ? \"지도\" : \"표\"}</span>\n                </button>\n              );\n            })}\n          </span>\n        )}\n      </div>\n\n      {/* 범례는 항상. 흐린 채움만 두면 어느 색이 무엇인지 알 수 없습니다 */}\n      <ul className={`${B}__legend`}>\n        {LEGEND.map((s) => (\n          <li key={s} className={`${B}__legend-item`}>\n            <span className={`${B}__legend-swatch`} data-status={statusAttr(s)} />\n            <span>{STATUS_WORD[s]}</span>\n          </li>\n        ))}\n      </ul>\n\n      <div className={`${B}__canvas`}>\n        <svg viewBox={geo.viewBox} role=\"img\" aria-label={mapLabel}>\n          {level === \"sido\"\n            ? geo.shapes.map((shape) => (\n                <g key={shape.code} className={`${B}__region`} data-region={shape.code} {...shapeProps(shape)}>\n                  {shape.paths.map((d, i) => (\n                    <path key={i} d={d} />\n                  ))}\n                </g>\n              ))\n            : geo.shapes.map((shape) => (\n                <path key={shape.code} className={`${B}__area`} data-area={shape.code} d={shape.paths[0]} {...shapeProps(shape)} />\n              ))}\n          {showLabels &&\n            geo.shapes\n              .filter((s) => level === \"gu\" || s.width >= LABEL_MIN_WIDTH)\n              .map((s) => (\n                <text key={`label-${s.code}`} className={`${B}__label`} x={s.cx.toFixed(1)} y={(s.cy + 3).toFixed(1)}>\n                  {s.code}\n                </text>\n              ))}\n        </svg>\n        {activeShape && (\n          // 요약이면서 다음 레벨의 입구\n          <div\n            className={`${B}__tooltip`}\n            data-state=\"open\"\n            role=\"status\"\n            style={{\n              left: `${Math.min(70, (activeShape.cx / vbW) * 100).toFixed(0)}%`,\n              top: `${Math.min(80, (activeShape.cy / vbH) * 100).toFixed(0)}%`,\n            }}\n          >\n            <p className={`${B}__tooltip-title`}>\n              <span className={`${B}__tooltip-dot`} data-status={statusAttr(activeStatus)} />\n              {activeShape.code}\n            </p>\n            <div className={`${B}__tooltip-row`}>\n              <span className={`${B}__tooltip-label`}>{STATUS_WORD[activeStatus]}</span>\n              {counts[activeShape.code] !== undefined && (\n                <span className={`${B}__tooltip-value`}>{counts[activeShape.code]}</span>\n              )}\n            </div>\n            {level === \"sido\" && onRegionSelect && (\n              <div className={`${B}__tooltip-row`} style={{ marginTop: 6 }}>\n                <span className={`${B}__tooltip-label`}>클릭하면 구별 지도로 들어갑니다</span>\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n\n      {/* 같은 상태를 행으로. 툴팁이 유일한 통로가 되지 않게 */}\n      {view === \"table\" && (\n        <div className={`${B}__table`}>\n          <table>\n            <thead>\n              <tr>\n                <th scope=\"col\">지역</th>\n                <th scope=\"col\">상태</th>\n                <th scope=\"col\">장애 건수</th>\n              </tr>\n            </thead>\n            <tbody>\n              {geo.shapes\n                .filter((s) => states[s.code] !== undefined)\n                .map((s) => {\n                  const status = states[s.code] ?? \"normal\";\n                  return (\n                    <tr key={s.code}>\n                      <th scope=\"row\">{s.code}</th>\n                      <td>\n                        <span>\n                          <span className={`${B}__tooltip-dot`} data-status={statusAttr(status)} />\n                          {STATUS_WORD[status]}\n                        </span>\n                      </td>\n                      <td>{counts[s.code] ?? 0}</td>\n                    </tr>\n                  );\n                })}\n            </tbody>\n          </table>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport const ShapeMap = Object.assign(ShapeMapRoot, {});\n",
      "type": "registry:ui",
      "target": "components/ui/shape-map.tsx"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/chip.css",
      "content": "/* ============================================================================\n   Element — Chip\n   ----------------------------------------------------------------------------\n   One pill, four behaviours. What changes between them is not the look but the\n   contract: whether the chip holds a state at all, and if so whether that state\n   is exclusive within its group.\n\n     button     상태 없음. 누르면 액션이 일어난다\n     toggle     혼자서 켜고 끈다\n     radio      그룹 안에서 하나만\n     checkbox   그룹 안에서 여러 개\n\n   So `--type_*` is a documentation marker for the author, and the real\n   difference lives in the markup each type demands — see the Specification\n   table. The selected treatment is shared: whatever the unselected variant was,\n   a selected chip fills solid.\n\n   atom-chip\n     --type_{button|toggle|radio|checkbox}   button is the default\n     --variant_{neutral-weak|neutral-outline|neutral-solid}\n                                             neutral-weak is the default\n     --size_{small|medium}                   medium is the default\n     [data-selected]  [aria-pressed=\"true\"]  __input:checked\n     [disabled]  [aria-disabled=\"true\"]\n     __input     visually-hidden radio/checkbox behind a <label>\n     __prefix    leading icon\n     __label\n     __check     선택 표시. 넣으면 선택됐을 때만 보인다\n     __caret     메뉴를 여는 button 타입에서\n     __count     뒤에 붙는 개수\n\n   atom-chip-group  (+ --wrap  --scroll)\n   ========================================================================== */\n\n.atom-chip {\n  position: relative;\n  display: inline-flex;\n  align-items: center;\n  gap: 4px;\n  height: 33px;\n  padding-inline: var(--atom-x3);\n  border-radius: var(--atom-radius-full);\n  font-size: 13px;\n  line-height: 1;\n  white-space: nowrap;\n  cursor: pointer;\n  user-select: none;\n  transition: background-color var(--atom-duration-fast) ease,\n    box-shadow var(--atom-duration-fast) ease,\n    color var(--atom-duration-fast) ease;\n}\n\n/* radio and checkbox chips are a real input behind a label, so the control is\n   removed from view but not from the accessibility tree or the tab order. */\n.atom-chip__input {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  opacity: 0;\n  pointer-events: none;\n}\n\n.atom-chip__label {\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.atom-chip__prefix {\n  width: 14px;\n  height: 14px;\n  flex-shrink: 0;\n  margin-inline-start: -2px;\n}\n\n.atom-chip__caret {\n  width: 12px;\n  height: 12px;\n  flex-shrink: 0;\n  margin-inline-end: -2px;\n  opacity: 0.7;\n  transition: transform var(--atom-duration-base) var(--atom-ease-out);\n}\n\n/* The check is opt-in and costs width, so it collapses instead of merely fading\n   — a row of chips that reserves space for a mark none of them shows reads as\n   badly aligned rather than as unselected. */\n.atom-chip__check {\n  width: 0;\n  height: 14px;\n  flex-shrink: 0;\n  opacity: 0;\n  transition: width var(--atom-duration-fast) ease,\n    opacity var(--atom-duration-fast) ease;\n}\n\n.atom-chip__count {\n  flex-shrink: 0;\n  font-variant-numeric: tabular-nums;\n  opacity: 0.7;\n}\n\n/* --- types — guardrails, not decoration ---------------------------------- */\n/* These modifiers mostly document which contract a chip follows; the real\n   difference is the markup each type demands. The two rules below are the\n   exceptions, and both exist to stop wrong markup from shipping a wrong signal.\n\n   A button chip holds no state, so a check on one can only mislead. */\n.atom-chip--type_button .atom-chip__check {\n  display: none;\n}\n\n/* A caret promises \"this opens a menu\". The stateful three change a value in\n   place, so a caret on them is a promise the chip does not keep. */\n.atom-chip--type_toggle .atom-chip__caret,\n.atom-chip--type_radio .atom-chip__caret,\n.atom-chip--type_checkbox .atom-chip__caret {\n  display: none;\n}\n\n/* --- variants — the unselected look -------------------------------------- */\n.atom-chip,\n.atom-chip--variant_neutral-weak {\n  background: var(--atom-bg-neutral-weak);\n  color: var(--atom-fg-neutral);\n}\n\n.atom-chip--variant_neutral-outline {\n  background: transparent;\n  box-shadow: inset 0 0 0 1px var(--atom-border-default);\n  color: var(--atom-fg-neutral);\n}\n\n.atom-chip--variant_neutral-solid {\n  background: var(--atom-bg-neutral-solid);\n  color: var(--atom-fg-on-solid);\n  font-weight: 600;\n}\n\n/* --- selected — shared by toggle, radio and checkbox ---------------------- */\n/* One rule for three types: whatever the chip looked like unselected, selecting\n   it fills solid. Fill is the only difference a chip can carry that survives\n   being read at a glance in a scrolling row. */\n.atom-chip[data-selected],\n.atom-chip[aria-pressed=\"true\"],\n.atom-chip:has(.atom-chip__input:checked) {\n  background: var(--atom-bg-neutral-solid);\n  box-shadow: none;\n  color: var(--atom-fg-on-solid);\n  font-weight: 600;\n}\n\n.atom-chip[data-selected] .atom-chip__check,\n.atom-chip[aria-pressed=\"true\"] .atom-chip__check,\n.atom-chip:has(.atom-chip__input:checked) .atom-chip__check {\n  width: 14px;\n  opacity: 1;\n}\n\n/* --- sizes --------------------------------------------------------------- */\n.atom-chip--size_small {\n  height: 28px;\n  padding-inline: var(--atom-x2);\n  font-size: 12px;\n}\n\n.atom-chip--size_small .atom-chip__prefix,\n.atom-chip--size_small .atom-chip__check {\n  height: 12px;\n}\n\n.atom-chip--size_small[data-selected] .atom-chip__check,\n.atom-chip--size_small[aria-pressed=\"true\"] .atom-chip__check,\n.atom-chip--size_small:has(.atom-chip__input:checked) .atom-chip__check {\n  width: 12px;\n}\n\n/* --- states -------------------------------------------------------------- */\n.atom-chip:hover:not([disabled]):not([aria-disabled=\"true\"]) {\n  filter: brightness(0.96);\n}\n\n.atom-chip[data-state=\"open\"] .atom-chip__caret {\n  transform: rotate(180deg);\n}\n\n.atom-chip:focus-visible,\n.atom-chip:has(.atom-chip__input:focus-visible) {\n  outline: 2px solid var(--atom-fg-brand);\n  outline-offset: 2px;\n}\n\n.atom-chip[disabled],\n.atom-chip[aria-disabled=\"true\"] {\n  opacity: 0.4;\n  cursor: not-allowed;\n}\n\n/* --- group --------------------------------------------------------------- */\n.atom-chip-group {\n  display: flex;\n  align-items: center;\n  gap: var(--atom-x2);\n  min-width: 0;\n}\n\n.atom-chip-group--wrap {\n  flex-wrap: wrap;\n  row-gap: var(--atom-x2);\n}\n\n/* A single-line rail. The row keeps its height when it scrolls, which a wrapping\n   group cannot promise, so filter bars pinned under a header use this one. */\n.atom-chip-group--scroll {\n  flex-wrap: nowrap;\n  overflow-x: auto;\n  scrollbar-width: none;\n}\n\n.atom-chip-group--scroll::-webkit-scrollbar {\n  display: none;\n}\n\n.atom-chip-group--scroll > .atom-chip {\n  flex-shrink: 0;\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/chip.css"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/shape-map.css",
      "content": "/* ============================================================================\n   Element — Shape Map\n   ----------------------------------------------------------------------------\n   An administrative map of South Korea drawn as SVG shapes, coloured by the\n   state of each region. Two levels come out of one geometry: 시·도 for the whole\n   country, and 시·군·구 when the reader drills into one of them.\n\n   The geometry is 251 시·군·구 polygons, projected once (Mercator) and\n   simplified. The 시·도 level does not use a second, merged set of 17 shapes —\n   it uses the same 251 paths, grouped, with each group's stroke set to its own\n   fill so the internal borders disappear. That is why the line colour only\n   shows where a border is meant to be read: change the level and the same paths\n   gain or lose their edges without swapping a single `d` attribute.\n\n   Surface and line are local custom properties, not hard-coded values. A\n   monitoring wall runs this on a dark board with a different ground than a\n   report page does, and both are the same component with two values changed.\n\n   Colour carries state, and state is never colour alone. The fills are the\n   status ramp's weak steps because a region is a large block — Foundations\n   reserves saturated fills for small marks — and the legend and the tooltip\n   both name the state in words beside a swatch.\n\n   atom-shape-map\n     [data-level=\"sido|gu\"]         which level is drawn\n     [data-view=\"map|table\"]        which twin is showing\n     --detail_district              sido level, with 구 borders drawn as a hairline\n     __header      __back\n     __canvas                       the svg box\n     __region      <g>              a 시·도. Owns state and the hover target at\n                                    the sido level\n     __area        <path>           a 시·군·구. Owns state at the gu level\n     __label                        a region name drawn on the map. Optional\n     __tooltip     __tooltip-title  __tooltip-row  __tooltip-dot\n                   __tooltip-label  __tooltip-value\n     __legend      __legend-item  __legend-swatch\n     __table                        the same states as rows\n     __empty\n\n   Local custom properties, not tokens:\n     --atom-shape-map-surface       the ground the shapes sit on\n     --atom-shape-map-line          the border colour where a border is read\n     --atom-shape-map-fill          a region with nothing to report\n     --atom-shape-map-ink          internal: the one value a shape's fill and\n                                   stroke both read, so a status attribute\n                                   paints it and closes its seams at once\n   ========================================================================== */\n\n.atom-shape-map {\n  --atom-shape-map-surface: var(--atom-bg-layer);\n  --atom-shape-map-line: var(--atom-border-default);\n  --atom-shape-map-fill: var(--atom-bg-neutral-weak);\n\n  display: flex;\n  flex-direction: column;\n  gap: var(--atom-x3);\n  min-width: 0;\n}\n\n/* --- canvas -------------------------------------------------------------- */\n.atom-shape-map__canvas {\n  position: relative;\n  min-width: 0;\n  border-radius: var(--atom-radius-r2);\n  background: var(--atom-shape-map-surface);\n  box-shadow: inset 0 0 0 1px var(--atom-border-subtle);\n}\n\n.atom-shape-map__canvas svg {\n  display: block;\n  width: 100%;\n  height: auto;\n  overflow: visible;\n}\n\n/* --- shapes -------------------------------------------------------------- */\n/* One value is set per shape and both fill and stroke read it, so a status\n   attribute paints the shape and closes its seams in the same stroke. */\n.atom-shape-map__region,\n.atom-shape-map__area {\n  --atom-shape-map-ink: var(--atom-shape-map-fill);\n\n  fill: var(--atom-shape-map-ink);\n  stroke: var(--atom-shape-map-ink);\n  stroke-width: 1.6;\n  stroke-linejoin: round;\n  cursor: pointer;\n  transition: fill var(--atom-duration-fast) ease;\n}\n\n.atom-shape-map__region path {\n  fill: inherit;\n  stroke: inherit;\n  stroke-width: inherit;\n  stroke-linejoin: inherit;\n}\n\n/* --- level --------------------------------------------------------------- */\n/* At the sido level the stroke stays the fill, so the 구 borders inside a\n   province close up and the province reads as one shape. Its outer edge is\n   drawn by where the neighbouring province's fill stops — there is nothing to\n   stroke.\n\n   `--detail_district` is the middle format: the province keeps its colour and\n   the 구 borders come back as a hairline, for a reader who needs to see the\n   subdivisions without leaving the country view. */\n.atom-shape-map--detail_district .atom-shape-map__region path,\n.atom-shape-map--detail_district .atom-shape-map__area {\n  stroke: var(--atom-shape-map-line);\n  stroke-width: 0.6;\n}\n\n/* At the gu level every area is its own shape and its border is meant to be\n   read, so the line colour takes over from the fill. */\n.atom-shape-map[data-level=\"gu\"] .atom-shape-map__area {\n  stroke: var(--atom-shape-map-line);\n  stroke-width: 1;\n}\n\n/* --- status -------------------------------------------------------------- */\n/* Weak steps, not solid. A 시·도 is a large block, and Foundations reserves\n   saturated fills for small marks and accents. The urgency comes from the\n   border, which is the status foreground at full strength. */\n.atom-shape-map [data-status=\"warning\"] {\n  --atom-shape-map-ink: var(--atom-bg-warning-weak);\n}\n\n.atom-shape-map [data-status=\"critical\"] {\n  --atom-shape-map-ink: var(--atom-bg-critical-weak);\n}\n\n.atom-shape-map[data-level=\"gu\"] .atom-shape-map__area[data-status=\"warning\"] {\n  stroke: var(--atom-fg-warning);\n}\n\n.atom-shape-map[data-level=\"gu\"] .atom-shape-map__area[data-status=\"critical\"] {\n  stroke: var(--atom-fg-critical);\n}\n\n/* Hover is a step, not a new colour: the state must not change under the\n   pointer. */\n.atom-shape-map__region:hover,\n.atom-shape-map__area:hover {\n  filter: brightness(0.96);\n}\n\n.atom-shape-map__region:focus-visible,\n.atom-shape-map__area:focus-visible {\n  outline: 2px solid var(--atom-fg-brand);\n  outline-offset: 1px;\n}\n\n/* --- label --------------------------------------------------------------- */\n/* Text ink, never the status colour — the shape beside it carries the state.\n   Labels belong to the level being read: a country view names provinces and a\n   district view names 구. Both at once is a wall of type. */\n.atom-shape-map__label {\n  fill: var(--atom-fg-neutral-muted);\n  font-size: 10px;\n  text-anchor: middle;\n  pointer-events: none;\n}\n\n/* --- tooltip ------------------------------------------------------------- */\n/* Also the click target that opens the next level. It enhances and does not\n   gate: the same states are in the table. */\n.atom-shape-map__tooltip {\n  position: absolute;\n  z-index: 10;\n  min-width: 148px;\n  padding: var(--atom-x2) var(--atom-x3);\n  border-radius: var(--atom-radius-r2);\n  background: var(--atom-bg-tooltip);\n  box-shadow: var(--atom-shadow-s2);\n  color: var(--atom-fg-tooltip);\n  font-size: 12px;\n}\n\n.atom-shape-map__tooltip[data-state=\"closed\"] {\n  display: none;\n}\n\n.atom-shape-map__tooltip-title {\n  display: flex;\n  align-items: center;\n  gap: var(--atom-x2);\n  margin: 0 0 4px;\n  font-weight: 600;\n}\n\n.atom-shape-map__tooltip-row {\n  display: flex;\n  align-items: center;\n  gap: var(--atom-x2);\n}\n\n/* The one place a status colour appears at full strength in the tooltip, with\n   the word right beside it. */\n.atom-shape-map__tooltip-dot {\n  width: 8px;\n  height: 8px;\n  flex-shrink: 0;\n  border-radius: var(--atom-radius-full);\n  background: var(--atom-fg-neutral-subtle);\n}\n\n.atom-shape-map__tooltip-dot[data-status=\"warning\"] {\n  background: var(--atom-fg-warning);\n}\n\n.atom-shape-map__tooltip-dot[data-status=\"critical\"] {\n  background: var(--atom-fg-critical);\n}\n\n.atom-shape-map__tooltip-label {\n  flex: 1;\n  min-width: 0;\n}\n\n.atom-shape-map__tooltip-value {\n  font-variant-numeric: tabular-nums;\n  font-weight: 600;\n}\n\n/* --- legend -------------------------------------------------------------- */\n/* Always present. Three pale fills with no legend asks the reader to guess\n   which one means what. */\n.atom-shape-map__legend {\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n  gap: var(--atom-x2) var(--atom-x4);\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n.atom-shape-map__legend-item {\n  display: flex;\n  align-items: center;\n  gap: var(--atom-x2);\n  font-size: 12px;\n  color: var(--atom-fg-neutral-muted);\n}\n\n.atom-shape-map__legend-swatch {\n  width: 12px;\n  height: 12px;\n  flex-shrink: 0;\n  border-radius: 3px;\n  background: var(--atom-bg-neutral-weak);\n  box-shadow: inset 0 0 0 1px var(--atom-border-default);\n}\n\n.atom-shape-map__legend-swatch[data-status=\"warning\"] {\n  background: var(--atom-bg-warning-weak);\n  box-shadow: inset 0 0 0 1px var(--atom-fg-warning);\n}\n\n.atom-shape-map__legend-swatch[data-status=\"critical\"] {\n  background: var(--atom-bg-critical-weak);\n  box-shadow: inset 0 0 0 1px var(--atom-fg-critical);\n}\n\n/* --- header · back ------------------------------------------------------- */\n.atom-shape-map__header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--atom-x3);\n}\n\n.atom-shape-map__toggle {\n  display: flex;\n  align-items: center;\n  gap: var(--atom-x2);\n  flex-shrink: 0;\n}\n\n.atom-shape-map__back {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n  flex-shrink: 0;\n  padding: 0;\n  border: 0;\n  background: none;\n  font: inherit;\n  font-size: 13px;\n  color: var(--atom-fg-neutral-muted);\n  cursor: pointer;\n}\n\n.atom-shape-map__back svg {\n  width: 14px;\n  height: 14px;\n  transform: rotate(180deg);\n}\n\n/* --- table --------------------------------------------------------------- */\n.atom-shape-map__table {\n  overflow-x: auto;\n}\n\n.atom-shape-map[data-view=\"map\"] .atom-shape-map__table,\n.atom-shape-map[data-view=\"table\"] .atom-shape-map__canvas,\n.atom-shape-map[data-view=\"table\"] .atom-shape-map__legend {\n  display: none;\n}\n\n.atom-shape-map__table table {\n  width: 100%;\n  border-collapse: collapse;\n  font-size: 13px;\n}\n\n.atom-shape-map__table th,\n.atom-shape-map__table td {\n  padding: var(--atom-x2) var(--atom-x3);\n  border-bottom: 1px solid var(--atom-border-subtle);\n  text-align: left;\n}\n\n.atom-shape-map__table th:last-child,\n.atom-shape-map__table td:last-child {\n  text-align: right;\n  font-variant-numeric: tabular-nums;\n}\n\n.atom-shape-map__table thead th {\n  font-weight: 600;\n  color: var(--atom-fg-neutral-muted);\n}\n\n/* A status word in the table gets its dot too, so the table is not a\n   colour-free downgrade of the map. */\n.atom-shape-map__table td span {\n  display: inline-flex;\n  align-items: center;\n  gap: var(--atom-x2);\n}\n\n/* --- empty --------------------------------------------------------------- */\n.atom-shape-map__empty {\n  display: grid;\n  place-items: center;\n  min-height: 200px;\n  font-size: 14px;\n  color: var(--atom-fg-neutral-subtle);\n}\n\n/* --- motion · forced colors ---------------------------------------------- */\n@media (prefers-reduced-motion: reduce) {\n  .atom-shape-map__region,\n  .atom-shape-map__area {\n    transition: none;\n  }\n}\n\n/* With hue taken away the legend and the table are what is left; the shapes get\n   a system outline so the borders still read. */\n@media (forced-colors: active) {\n  .atom-shape-map__region path,\n  .atom-shape-map__area {\n    stroke: CanvasText;\n    stroke-width: 1;\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/shape-map.css"
    }
  ],
  "categories": [
    "display"
  ],
  "meta": {
    "version": "0.1.0",
    "kind": "component",
    "status": "stable",
    "specSource": "measured",
    "parts": [
      "ShapeMap"
    ],
    "docs": "poc/design-system/components/shape-map.md"
  }
}
