{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "switch",
  "type": "registry:ui",
  "title": "Switch",
  "description": "켜는 즉시 반영되는 설정을 켜고 끄는 토글입니다.",
  "registryDependencies": [
    "@atom/tokens",
    "@atom/lib-bem",
    "@atom/lib-cn",
    "@atom/lib-field-context",
    "@atom/lib-use-controllable-state"
  ],
  "files": [
    {
      "path": "poc/design-system/implement/src/components/switch.tsx",
      "content": "\"use client\";\n\nimport { type InputHTMLAttributes, type ReactNode, type Ref, useId } from \"react\";\nimport { mod } from \"../lib/bem\";\nimport { cn } from \"../lib/cn\";\nimport { useFieldControl } from \"../lib/field-context\";\nimport { useControllableState } from \"../lib/use-controllable-state\";\n\n/* -------------------------------------------------------------------------- */\n/*  Switch (components/switch.md, styles/atom/elements/switch.css)            */\n/*    <Switch aria-label=\"알림\" checked={on} onCheckedChange={setOn} />        */\n/*    <Switch label=\"알림\" description=\"변경 사항을 바로 알려드려요\" defaultChecked /> */\n/* -------------------------------------------------------------------------- */\n\nexport type SwitchSize = \"small\" | \"medium\";\n\nconst B = \"atom-switch\";\nconst F = \"atom-switch-field\";\n\nexport type SwitchProps = Omit<InputHTMLAttributes<HTMLInputElement>, \"size\" | \"type\" | \"checked\" | \"defaultChecked\"> & {\n  ref?: Ref<HTMLInputElement>;\n  /** 켜짐 상태입니다. */\n  checked?: boolean;\n  /** uncontrolled 로 쓸 때의 처음 켜짐 상태입니다. */\n  defaultChecked?: boolean;\n  /** Track · Thumb 크기와 이동 거리를 함께 결정합니다. */\n  size?: SwitchSize;\n  /** 조작할 수 없게 만듭니다. 라벨 행이 있으면 행 전체가 흐려집니다. */\n  disabled?: boolean;\n  /** 라벨 행의 왼쪽 텍스트입니다. 없으면 `aria-label` 이 필요합니다. */\n  label?: ReactNode;\n  /** 라벨 아래에 놓이는 보충 설명입니다. */\n  description?: ReactNode;\n  /** 상태가 바뀔 때 호출됩니다. */\n  onCheckedChange?: (checked: boolean) => void;\n  /** 라벨이 시각적으로 없을 때 필요합니다. */\n  \"aria-label\"?: string;\n};\n\n/**\n * 켜는 즉시 반영되는 설정 토글. 실제 `<input type=\"checkbox\" role=\"switch\">` 를 감춰 두고 Track(`atom-switch`)이 감쌉니다.\n * `label` 이 있으면 행 전체가 `<label class=\"atom-switch-field\">` 가 되고 Switch 는 그 안의 `<span>` 입니다.\n * className 은 가장 바깥 요소(Switch Field 또는 Track)에 붙습니다.\n */\nfunction SwitchRoot({\n  ref,\n  checked: checkedProp,\n  defaultChecked = false,\n  size = \"medium\",\n  disabled = false,\n  label,\n  description,\n  onCheckedChange,\n  \"aria-label\": ariaLabel,\n  \"aria-describedby\": ariaDescribedBy,\n  id,\n  required,\n  className,\n  onChange,\n  ...rest\n}: SwitchProps) {\n  const field = useFieldControl();\n  const [checked, setChecked] = useControllableState({\n    value: checkedProp,\n    defaultValue: defaultChecked,\n    onChange: onCheckedChange,\n  });\n  const isDisabled = disabled || !!field?.disabled;\n  const hasLabel = label !== undefined && label !== null;\n  const hasDescription = description !== undefined && description !== null;\n  const labelId = useId();\n  const descriptionId = useId();\n\n  const input = (\n    <input\n      {...rest}\n      ref={ref}\n      className={`${B}__input`}\n      type=\"checkbox\"\n      role=\"switch\"\n      id={id ?? field?.controlId}\n      required={required ?? (field?.required || undefined)}\n      checked={checked}\n      disabled={isDisabled}\n      aria-label={ariaLabel}\n      // 행 전체가 <label> 이라 설명까지 이름에 섞이므로, 이름은 라벨 텍스트로 · 설명은 describedby 로 나눕니다\n      aria-labelledby={hasLabel && hasDescription && !ariaLabel ? labelId : undefined}\n      aria-describedby={cn(ariaDescribedBy, hasLabel && hasDescription && descriptionId, field?.describedBy) || undefined}\n      onChange={(e) => {\n        onChange?.(e);\n        setChecked(e.currentTarget.checked);\n      }}\n    />\n  );\n  const thumb = <span className={`${B}__thumb`} />;\n  const trackClass = cn(B, mod(B, \"size\", size, \"medium\"));\n\n  if (!hasLabel) {\n    // 단독 — Track 자신이 <label>. 상태는 __input 의 :checked · :disabled 가 전달합니다 (data-state 는 정적 그림 전용)\n    return (\n      <label className={cn(trackClass, className)}>\n        {input}\n        {thumb}\n      </label>\n    );\n  }\n\n  // Switch Field — 행 전체가 <label>, 비활성은 행에 겁니다\n  return (\n    <label className={cn(F, className)} data-disabled={isDisabled ? \"\" : undefined}>\n      <span className={`${F}__text`}>\n        <span id={labelId} className={`${F}__label`}>\n          {label}\n        </span>\n        {hasDescription && (\n          <span id={descriptionId} className={`${F}__description`}>\n            {description}\n          </span>\n        )}\n      </span>\n      <span className={trackClass}>\n        {input}\n        {thumb}\n      </span>\n    </label>\n  );\n}\n\nexport const Switch = Object.assign(SwitchRoot, {});\n",
      "type": "registry:ui",
      "target": "components/ui/switch.tsx"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/button.css",
      "content": "/* ============================================================================\n   Element — Button\n   ----------------------------------------------------------------------------\n   atom-button\n     --size_{xsmall|small|medium|large}      medium is the default\n     --variant_{brand-solid|neutral-solid|neutral-weak|critical-solid\n                |brand-outline|neutral-outline|ghost}\n     --layout_icon-only\n     --width_full\n   states: :hover  [data-pressed]  [disabled]  [data-loading]\n\n   Loaded by both the card preview and the detail page, so\n   this file is the only definition of the component.\n   ========================================================================== */\n\n.atom-button {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  gap: var(--atom-x2);\n  height: 47px;\n  padding-inline: var(--atom-x5);\n  border-radius: var(--atom-radius-r2);\n  font-size: 15px;\n  font-weight: 600;\n  line-height: 1;\n  white-space: nowrap;\n  user-select: none;\n  transition: filter var(--atom-duration-fast) ease,\n    background-color var(--atom-duration-fast) ease;\n}\n\n.atom-button__icon {\n  width: 18px;\n  height: 18px;\n  flex-shrink: 0;\n}\n\n.atom-button__label {\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n/* --- variants ------------------------------------------------------------- */\n.atom-button--variant_neutral-solid {\n  background: var(--atom-bg-neutral-solid);\n  color: var(--atom-fg-on-solid);\n}\n\n.atom-button--variant_brand-solid {\n  background: var(--atom-primary-600);\n  color: #fff;\n}\n\n.atom-button--variant_critical-solid {\n  background: var(--atom-bg-critical-solid);\n  color: #fff;\n}\n\n.atom-button--variant_neutral-weak {\n  background: var(--atom-bg-neutral-weak);\n  color: var(--atom-fg-neutral);\n}\n\n.atom-button--variant_brand-outline {\n  background: transparent;\n  color: var(--atom-primary-600);\n  box-shadow: inset 0 0 0 1px currentColor;\n}\n\n.atom-button--variant_neutral-outline {\n  background: transparent;\n  color: var(--atom-fg-neutral);\n  box-shadow: inset 0 0 0 1px var(--atom-border-default);\n}\n\n.atom-button--variant_ghost {\n  background: transparent;\n  color: var(--atom-fg-neutral);\n}\n\n/* --- sizes --------------------------------------------------------------- */\n.atom-button--size_xsmall {\n  height: 32px;\n  gap: var(--atom-x1);\n  padding-inline: var(--atom-x3);\n  border-radius: var(--atom-radius-full);\n  font-size: 13px;\n}\n\n.atom-button--size_xsmall .atom-button__icon {\n  width: 14px;\n  height: 14px;\n}\n\n.atom-button--size_small {\n  height: 40px;\n  padding-inline: var(--atom-x4);\n  font-size: 14px;\n}\n\n.atom-button--size_small .atom-button__icon {\n  width: 16px;\n  height: 16px;\n}\n\n.atom-button--size_large {\n  height: 54px;\n  padding-inline: var(--atom-x6);\n  border-radius: var(--atom-radius-r3);\n  font-size: 16px;\n}\n\n.atom-button--size_large .atom-button__icon {\n  width: 20px;\n  height: 20px;\n}\n\n/* --- layout -------------------------------------------------------------- */\n.atom-button--layout_icon-only {\n  width: 47px;\n  padding-inline: 0;\n}\n\n.atom-button--layout_icon-only.atom-button--size_xsmall {\n  width: 32px;\n}\n\n.atom-button--layout_icon-only.atom-button--size_small {\n  width: 40px;\n}\n\n.atom-button--layout_icon-only.atom-button--size_large {\n  width: 54px;\n}\n\n.atom-button--width_full {\n  width: 100%;\n}\n\n/* --- states -------------------------------------------------------------- */\n.atom-button:hover:not([disabled]):not([data-loading]) {\n  filter: brightness(0.94);\n}\n\n.atom-button--variant_ghost:hover:not([disabled]),\n.atom-button--variant_neutral-outline:hover:not([disabled]),\n.atom-button--variant_brand-outline:hover:not([disabled]) {\n  filter: none;\n  background: var(--atom-bg-neutral-weak);\n}\n\n.atom-button[data-pressed] {\n  filter: brightness(0.88);\n}\n\n.atom-button[disabled] {\n  opacity: 0.35;\n  cursor: not-allowed;\n  filter: none;\n}\n\n.atom-button[data-loading] {\n  cursor: progress;\n}\n\n.atom-button[data-loading] .atom-button__label {\n  opacity: 0.45;\n}\n\n.atom-button__spinner {\n  width: 16px;\n  height: 16px;\n  flex-shrink: 0;\n  border-radius: var(--atom-radius-full);\n  border: 2px solid currentColor;\n  border-top-color: transparent;\n  animation: atom-button-spin 700ms linear infinite;\n}\n\n@keyframes atom-button-spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/button.css"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/checkbox.css",
      "content": "/* ============================================================================\n   Element — Checkbox\n   ----------------------------------------------------------------------------\n   atom-checkbox\n     --size_{small|medium|large}             medium is the default\n     --align_top                             label wraps to more than one line\n     [data-state=\"unchecked|checked|indeterminate\"]\n     [data-invalid]  [data-disabled]\n     __control  __check  __indeterminate  __label  __description\n\n   atom-checkbox-group  (+ --layout_horizontal)\n\n   Reads as a real control: <label> + a visually-hidden <input type=\"checkbox\">\n   also works, since :checked is wired up alongside the data-state attribute.\n   ========================================================================== */\n\n.atom-checkbox {\n  display: inline-flex;\n  align-items: center;\n  gap: 10px;\n  font-size: 15px;\n  line-height: 1.45;\n  color: var(--atom-fg-neutral);\n  cursor: pointer;\n}\n\n.atom-checkbox--align_top {\n  align-items: flex-start;\n}\n\n.atom-checkbox__input {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  opacity: 0;\n  pointer-events: none;\n}\n\n.atom-checkbox__control {\n  position: relative;\n  width: 22px;\n  height: 22px;\n  flex-shrink: 0;\n  border-radius: var(--atom-radius-r1);\n  background: var(--atom-bg-layer);\n  box-shadow: inset 0 0 0 1.5px var(--atom-border-default);\n  transition: background-color var(--atom-duration-fast) ease,\n    box-shadow var(--atom-duration-fast) ease;\n}\n\n.atom-checkbox--align_top .atom-checkbox__control {\n  margin-top: 1px;\n}\n\n.atom-checkbox__check,\n.atom-checkbox__indeterminate {\n  position: absolute;\n  inset: 0;\n  margin: auto;\n  color: var(--atom-fg-on-solid);\n  opacity: 0;\n  transition: opacity var(--atom-duration-fast) ease;\n}\n\n.atom-checkbox__check {\n  width: 14px;\n  height: 14px;\n}\n\n.atom-checkbox__indeterminate {\n  width: 10px;\n  height: 2px;\n  border-radius: var(--atom-radius-full);\n  background: currentColor;\n}\n\n.atom-checkbox__text {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n  min-width: 0;\n}\n\n.atom-checkbox__description {\n  font-size: 13px;\n  color: var(--atom-fg-neutral-muted);\n  word-break: keep-all;\n}\n\n/* --- sizes --------------------------------------------------------------- */\n.atom-checkbox--size_small {\n  gap: 8px;\n  font-size: 14px;\n}\n\n.atom-checkbox--size_small .atom-checkbox__control {\n  width: 18px;\n  height: 18px;\n}\n\n.atom-checkbox--size_small .atom-checkbox__check {\n  width: 12px;\n  height: 12px;\n}\n\n.atom-checkbox--size_large {\n  gap: 12px;\n  font-size: 16px;\n}\n\n.atom-checkbox--size_large .atom-checkbox__control {\n  width: 26px;\n  height: 26px;\n  border-radius: 5px;\n}\n\n.atom-checkbox--size_large .atom-checkbox__check {\n  width: 17px;\n  height: 17px;\n}\n\n/* --- states -------------------------------------------------------------- */\n.atom-checkbox[data-state=\"checked\"] .atom-checkbox__control,\n.atom-checkbox[data-state=\"indeterminate\"] .atom-checkbox__control,\n.atom-checkbox__input:checked + .atom-checkbox__control {\n  background: var(--atom-bg-neutral-solid);\n  box-shadow: inset 0 0 0 1.5px var(--atom-bg-neutral-solid);\n}\n\n.atom-checkbox[data-state=\"checked\"] .atom-checkbox__check,\n.atom-checkbox__input:checked + .atom-checkbox__control .atom-checkbox__check {\n  opacity: 1;\n}\n\n.atom-checkbox[data-state=\"indeterminate\"] .atom-checkbox__indeterminate {\n  opacity: 1;\n}\n\n.atom-checkbox:hover:not([data-disabled]) .atom-checkbox__control {\n  box-shadow: inset 0 0 0 1.5px var(--atom-fg-neutral-subtle);\n}\n\n.atom-checkbox[data-state=\"checked\"]:hover:not([data-disabled])\n  .atom-checkbox__control,\n.atom-checkbox[data-state=\"indeterminate\"]:hover:not([data-disabled])\n  .atom-checkbox__control {\n  box-shadow: inset 0 0 0 1.5px var(--atom-bg-neutral-solid);\n  filter: brightness(0.9);\n}\n\n.atom-checkbox__input:focus-visible + .atom-checkbox__control {\n  outline: 2px solid var(--atom-primary-600);\n  outline-offset: 2px;\n}\n\n.atom-checkbox[data-invalid] .atom-checkbox__control {\n  box-shadow: inset 0 0 0 1.5px var(--atom-fg-critical);\n}\n\n.atom-checkbox[data-invalid] .atom-checkbox__description {\n  color: var(--atom-fg-critical);\n}\n\n.atom-checkbox[data-disabled] {\n  opacity: 0.4;\n  cursor: not-allowed;\n}\n\n/* --- group --------------------------------------------------------------- */\n.atom-checkbox-group {\n  display: flex;\n  flex-direction: column;\n  gap: var(--atom-x4);\n}\n\n.atom-checkbox-group--layout_horizontal {\n  flex-direction: row;\n  flex-wrap: wrap;\n  gap: var(--atom-x5);\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/checkbox.css"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/switch.css",
      "content": "/* ============================================================================\n   Element — Switch\n   ----------------------------------------------------------------------------\n   The element itself is the track and the thumb rides inside it, so one shape\n   covers two jobs: a static <span data-state=\"checked\"> for card previews and\n   doc figures, and a <label> around a real <input type=\"checkbox\"> for anything\n   a user can actually touch. Every state below is declared for both forms.\n\n   atom-switch\n     --size_{small|medium}                   medium is the default\n     [data-state=\"unchecked|checked\"]        __input:checked\n     [data-disabled]                         __input:disabled\n     __input   visually-hidden checkbox — carries state, focus and form value\n     __thumb\n\n   atom-switch-field\n     [data-disabled]\n     __text  __label  __description\n\n   There is no invalid state on purpose: a switch commits the moment it moves,\n   so no submit step exists that could fail validation. See the Guidelines.\n   ========================================================================== */\n\n.atom-switch {\n  /* One number drives the size axis — track width is padding + thumb + travel,\n     so the two sizes differ by this value and the thumb diameter alone. */\n  --switch-travel: 22px;\n\n  position: relative;\n  display: inline-flex;\n  align-items: center;\n  width: 53px;\n  height: 31px;\n  flex-shrink: 0;\n  padding: 2px;\n  border-radius: var(--atom-radius-full);\n  background: var(--atom-gray-400);\n  cursor: pointer;\n  transition: background-color var(--atom-duration-base) var(--atom-ease-out),\n    filter var(--atom-duration-fast) ease;\n}\n\n/* Hidden from view but not from the tab order or the accessibility tree — the\n   same treatment Checkbox gives its input. display:none would drop both. */\n.atom-switch__input {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  opacity: 0;\n  pointer-events: none;\n}\n\n/* Fixed white rather than a surface token: the off track is grey in both\n   themes, so anything that inverts for dark would sink the thumb into it.\n   The shadow is hardcoded for the same reason — --atom-shadow-s1 is 6% black\n   and vanishes under a 27px circle on mid-grey; the knob has to read as raised\n   rather than as a hole punched in the track. */\n.atom-switch__thumb {\n  width: 27px;\n  height: 27px;\n  border-radius: var(--atom-radius-full);\n  background: #fff;\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);\n  transform: translateX(0);\n  transition: transform var(--atom-duration-base) var(--atom-ease-out),\n    background-color var(--atom-duration-base) var(--atom-ease-out);\n}\n\n/* --- sizes --------------------------------------------------------------- */\n.atom-switch--size_small {\n  --switch-travel: 16px;\n\n  width: 40px;\n  height: 24px;\n}\n\n.atom-switch--size_small .atom-switch__thumb {\n  width: 20px;\n  height: 20px;\n}\n\n/* --- checked ------------------------------------------------------------- */\n.atom-switch[data-state=\"checked\"],\n.atom-switch:has(.atom-switch__input:checked) {\n  background: var(--atom-bg-neutral-solid);\n}\n\n/* On, the track *is* bg-neutral-solid, and that token inverts between themes —\n   a fixed white thumb would disappear into the near-white dark-mode track. The\n   matching on-solid foreground is the only value that survives both. */\n.atom-switch[data-state=\"checked\"] .atom-switch__thumb,\n.atom-switch:has(.atom-switch__input:checked) .atom-switch__thumb {\n  transform: translateX(var(--switch-travel));\n  background: var(--atom-fg-on-solid);\n}\n\n/* --- states -------------------------------------------------------------- */\n.atom-switch:hover:not([data-disabled]):not(\n    :has(.atom-switch__input:disabled)\n  ) {\n  filter: brightness(0.94);\n}\n\n.atom-switch:active:not([data-disabled]):not(\n    :has(.atom-switch__input:disabled)\n  ) {\n  filter: brightness(0.88);\n}\n\n/* The hidden input takes focus, so the ring has to be drawn on the track that\n   wraps it. Same outline as Checkbox and Radio. */\n.atom-switch:focus-visible,\n.atom-switch:has(.atom-switch__input:focus-visible) {\n  outline: 2px solid var(--atom-primary-600);\n  outline-offset: 2px;\n}\n\n.atom-switch[data-disabled],\n.atom-switch:has(.atom-switch__input:disabled) {\n  opacity: 0.4;\n  cursor: not-allowed;\n}\n\n/* --- field — the label row ----------------------------------------------- */\n/* Switch never draws its own label, and Field is the wrong wrapper for it:\n   Field stacks a 13px caption above the control and reserves an error slot a\n   switch has no use for. A settings row is the opposite shape — body-sized\n   label on the left, control pinned to the far right, no error.\n   Making the row itself the <label> also fixes the touch target: the medium\n   track is 31px tall, under the 44px minimum, but the row is not. */\n.atom-switch-field {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--atom-x4);\n  width: 100%;\n  min-height: 44px;\n  color: var(--atom-fg-neutral);\n  cursor: pointer;\n}\n\n.atom-switch-field__text {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n  min-width: 0;\n}\n\n.atom-switch-field__label {\n  font-size: 15px;\n  line-height: 1.45;\n}\n\n.atom-switch-field__description {\n  font-size: 13px;\n  line-height: 1.5;\n  color: var(--atom-fg-neutral-muted);\n  word-break: keep-all;\n}\n\n/* The whole row is the label, so hovering the text is hovering the control.\n   Both rules out-specify the ones on .atom-switch itself, so the pressed step\n   has to be restated here or a press inside a row would read as a hover. */\n.atom-switch-field:hover:not([data-disabled]):not(\n    :has(.atom-switch__input:disabled)\n  )\n  .atom-switch {\n  filter: brightness(0.94);\n}\n\n.atom-switch-field:active:not([data-disabled]):not(\n    :has(.atom-switch__input:disabled)\n  )\n  .atom-switch {\n  filter: brightness(0.88);\n}\n\n/* Dim the row, not the switch: a bright label next to a faded switch reads as\n   \"the label is still live\", which is the wrong answer for the whole row. */\n.atom-switch-field[data-disabled],\n.atom-switch-field:has(.atom-switch__input:disabled) {\n  opacity: 0.4;\n  cursor: not-allowed;\n}\n\n/* …which means the switch inside must not dim a second time and land at 0.16. */\n.atom-switch-field[data-disabled] .atom-switch,\n.atom-switch-field:has(.atom-switch__input:disabled) .atom-switch {\n  opacity: 1;\n}\n\n/* --- motion -------------------------------------------------------------- */\n/* base.css already flattens every transition site-wide under reduce, but this\n   file also ships on its own, and the thumb's travel is the entire feedback of\n   the component. The jump is intentional: the position still changes, only the\n   animation between the two ends is dropped. */\n@media (prefers-reduced-motion: reduce) {\n  .atom-switch,\n  .atom-switch__thumb {\n    transition-duration: 0.01ms;\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/switch.css"
    }
  ],
  "categories": [
    "action-input"
  ],
  "meta": {
    "version": "0.1.0",
    "kind": "element",
    "status": "stable",
    "specSource": "measured",
    "parts": [
      "Switch"
    ],
    "docs": "poc/design-system/components/switch.md"
  }
}
