{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "type": "registry:ui",
  "title": "Button",
  "description": "명확한 액션을 쉽게 수행할 수 있도록 돕는 기본 인터랙션 컴포넌트입니다.",
  "registryDependencies": [
    "@atom/tokens",
    "@atom/lib-bem",
    "@atom/lib-cn",
    "@atom/lib-context",
    "@atom/lib-slot"
  ],
  "files": [
    {
      "path": "poc/design-system/implement/src/components/button.tsx",
      "content": "\"use client\";\n\nimport {\n  Children,\n  type ButtonHTMLAttributes,\n  type HTMLAttributes,\n  type ReactNode,\n  type Ref,\n  isValidElement,\n  useState,\n} from \"react\";\nimport { mod } from \"../lib/bem\";\nimport { cn } from \"../lib/cn\";\nimport { createStrictContext } from \"../lib/context\";\nimport { Slot } from \"../lib/slot\";\n\n/* -------------------------------------------------------------------------- */\n/*  Button (components/button.md, styles/atom/elements/button.css)            */\n/*    <Button variant=\"brandSolid\">                                           */\n/*      <Button.Icon><PlusIcon /></Button.Icon>                               */\n/*      글쓰기                                                                 */\n/*    </Button>                                                               */\n/*    <Button layout=\"iconOnly\" icon={<CloseIcon />} aria-label=\"닫기\" />      */\n/* -------------------------------------------------------------------------- */\n\nexport type ButtonSize = \"xsmall\" | \"small\" | \"medium\" | \"large\";\nexport type ButtonVariant =\n  | \"brandSolid\"\n  | \"neutralSolid\"\n  | \"criticalSolid\"\n  | \"neutralWeak\"\n  | \"brandOutline\"\n  | \"neutralOutline\"\n  | \"ghost\";\nexport type ButtonLayout = \"withText\" | \"iconOnly\";\nexport type ButtonWidth = \"fixed\" | \"full\";\n\nconst B = \"atom-button\";\n\nconst [ButtonContext, useButtonContext] = createStrictContext<{ layout: ButtonLayout }>(\"Button\");\n\nexport type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {\n  ref?: Ref<HTMLButtonElement>;\n  /** 버튼 높이와 좌우 여백, 폰트 크기를 결정합니다. */\n  size?: ButtonSize;\n  /** 강조도와 색을 결정합니다. */\n  variant?: ButtonVariant;\n  /** `iconOnly` 는 정사각형이 되고 라벨을 숨깁니다. aria-label 필수 */\n  layout?: ButtonLayout;\n  /** `full` 은 부모 너비를 모두 차지합니다. */\n  width?: ButtonWidth;\n  /** 스피너를 노출하고 입력을 무시합니다. */\n  loading?: boolean;\n  /** Icon Button 의 아이콘 (layout=\"iconOnly\"). `<Button.Icon>` 으로 감싸 렌더합니다. */\n  icon?: ReactNode;\n};\n\n/** 아이콘이 아닌 연속된 자식을 atom-button__label 로 묶습니다 (Prefix / Label / Suffix 순서 유지). */\nfunction wrapLabel(children: ReactNode, hideLabel: boolean): ReactNode[] {\n  const out: ReactNode[] = [];\n  let run: ReactNode[] = [];\n  const flush = () => {\n    if (run.length && !hideLabel) {\n      out.push(\n        <span key={`label-${out.length}`} className={`${B}__label`}>\n          {run}\n        </span>,\n      );\n    }\n    run = [];\n  };\n  Children.forEach(children, (child) => {\n    if (isValidElement(child) && child.type === ButtonIcon) {\n      flush();\n      out.push(child);\n    } else if (child !== null && child !== undefined && child !== false) {\n      run.push(child);\n    }\n  });\n  flush();\n  return out;\n}\n\nfunction ButtonRoot({\n  ref,\n  size = \"medium\",\n  variant = \"neutralSolid\",\n  layout = \"withText\",\n  width = \"fixed\",\n  loading = false,\n  disabled = false,\n  icon,\n  type = \"button\",\n  className,\n  children,\n  onClick,\n  onPointerDown,\n  onPointerUp,\n  onPointerLeave,\n  onKeyDown,\n  onKeyUp,\n  ...rest\n}: ButtonProps) {\n  const [pressed, setPressed] = useState(false);\n  const iconOnly = layout === \"iconOnly\";\n  const content = wrapLabel(\n    [icon !== undefined && <ButtonIcon key=\"icon\">{icon}</ButtonIcon>, ...Children.toArray(children)],\n    iconOnly,\n  );\n  // loading 이면 아이콘 자리에 스피너 (아이콘이 없으면 라벨 앞에)\n  const firstIcon = content.findIndex((n) => isValidElement(n) && n.type === ButtonIcon);\n  const spinner = <span key=\"spinner\" className={`${B}__spinner`} aria-hidden=\"true\" />;\n  if (loading) {\n    if (firstIcon >= 0) content.splice(firstIcon, 1, spinner);\n    else content.unshift(spinner);\n  }\n\n  return (\n    <ButtonContext value={{ layout }}>\n      <button\n        {...rest}\n        ref={ref}\n        type={type}\n        disabled={disabled}\n        aria-busy={loading || undefined}\n        data-loading={loading ? \"\" : undefined}\n        data-pressed={pressed && !disabled && !loading ? \"\" : undefined}\n        className={cn(\n          B,\n          mod(B, \"size\", size, \"medium\"),\n          mod(B, \"variant\", variant),\n          mod(B, \"layout\", layout, \"withText\"),\n          mod(B, \"width\", width, \"fixed\"),\n          className,\n        )}\n        onClick={(e) => {\n          // data-loading 은 겉모습만 바꾸므로 입력 무시는 여기서\n          if (loading) {\n            e.preventDefault();\n            return;\n          }\n          onClick?.(e);\n        }}\n        onPointerDown={(e) => {\n          setPressed(true);\n          onPointerDown?.(e);\n        }}\n        onPointerUp={(e) => {\n          setPressed(false);\n          onPointerUp?.(e);\n        }}\n        onPointerLeave={(e) => {\n          setPressed(false);\n          onPointerLeave?.(e);\n        }}\n        onKeyDown={(e) => {\n          if (e.key === \" \" || e.key === \"Enter\") setPressed(true);\n          onKeyDown?.(e);\n        }}\n        onKeyUp={(e) => {\n          setPressed(false);\n          onKeyUp?.(e);\n        }}\n      >\n        {content}\n      </button>\n    </ButtonContext>\n  );\n}\n\n/**\n * Prefix / Suffix Icon. 라벨 앞에 두면 Prefix, 뒤에 두면 Suffix. 장식용이라 스크린리더에서 숨깁니다.\n * button.css 가 크기를 아이콘 요소(svg)에 직접 주므로, 자식 요소에 atom-button__icon 을 합칩니다.\n */\nfunction ButtonIcon({ className, children, ...rest }: HTMLAttributes<HTMLElement>) {\n  useButtonContext(\"Button.Icon\");\n  return (\n    <Slot {...rest} aria-hidden=\"true\" className={cn(`${B}__icon`, className)}>\n      {children}\n    </Slot>\n  );\n}\n\nexport const Button = Object.assign(ButtonRoot, { Icon: ButtonIcon });\n",
      "type": "registry:ui",
      "target": "components/ui/button.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"
    }
  ],
  "categories": [
    "action-input"
  ],
  "meta": {
    "version": "0.1.0",
    "kind": "element",
    "status": "stable",
    "specSource": "measured",
    "parts": [
      "Button",
      "Button.Icon"
    ],
    "docs": "poc/design-system/components/button.md"
  }
}
