{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "accordion",
  "type": "registry:ui",
  "title": "Accordion",
  "description": "여러 항목을 접고 펼쳐 필요한 내용만 보여주는 컴포넌트입니다.",
  "registryDependencies": [
    "@atom/tokens",
    "@atom/icons",
    "@atom/lib-bem",
    "@atom/lib-cn",
    "@atom/lib-context",
    "@atom/lib-use-controllable-state"
  ],
  "files": [
    {
      "path": "poc/design-system/implement/src/components/accordion.tsx",
      "content": "\"use client\";\n\nimport {\n  type ButtonHTMLAttributes,\n  type HTMLAttributes,\n  type ReactNode,\n  type Ref,\n  useId,\n} from \"react\";\nimport { mod } from \"../lib/bem\";\nimport { cn } from \"../lib/cn\";\nimport { createStrictContext } from \"../lib/context\";\nimport { useControllableState } from \"../lib/use-controllable-state\";\nimport { ChevronDownIcon } from \"./icons\";\n\n/* -------------------------------------------------------------------------- */\n/*  Accordion (components/accordion.md, styles/atom/elements/accordion.css)   */\n/*    <Accordion items={[{ value: \"a\", title: \"제목\", content: \"본문\" }, …]} />  */\n/*                                                                            */\n/*    <Accordion type=\"single\" variant=\"flush\">                               */\n/*      <Accordion.Item value=\"a\">                                            */\n/*        <Accordion.Trigger>등록을 취소할 수 있나요?</Accordion.Trigger>        */\n/*        <Accordion.Content>7일 이내에 신청할 수 있어요.</Accordion.Content>     */\n/*      </Accordion.Item>                                                     */\n/*    </Accordion>                                                            */\n/* -------------------------------------------------------------------------- */\n\nexport type AccordionType = \"multiple\" | \"single\";\nexport type AccordionVariant = \"card\" | \"flush\";\nexport type AccordionHeadingLevel = 2 | 3 | 4 | 5 | 6;\nexport type AccordionItemData = {\n  value: string;\n  title: ReactNode;\n  content: ReactNode;\n  disabled?: boolean;\n};\n\nconst B = \"atom-accordion\";\n\ntype RootContext = {\n  openValues: string[];\n  toggle: (value: string) => void;\n  headingLevel: AccordionHeadingLevel;\n};\nconst [AccordionContext, useAccordionContext] = createStrictContext<RootContext>(\"Accordion\");\n\ntype ItemContext = { value: string; open: boolean; disabled: boolean; triggerId: string; panelId: string };\nconst [AccordionItemContext, useAccordionItemContext] = createStrictContext<ItemContext>(\"Accordion.Item\");\n\nexport type AccordionProps = Omit<HTMLAttributes<HTMLDivElement>, \"defaultValue\"> & {\n  ref?: Ref<HTMLDivElement>;\n  /** 항목 목록입니다. 최소 2개. (children 으로 Accordion.Item 을 직접 조립해도 됩니다) */\n  items?: AccordionItemData[];\n  /** 동시에 열 수 있는 개수입니다. */\n  type?: AccordionType;\n  /** Container 의 표면 처리입니다. */\n  variant?: AccordionVariant;\n  /** 열려 있는 항목입니다. `type` 이 `single` 이면 문자열 하나입니다. (제어 모드, 기본 `[]`) */\n  value?: string[] | string;\n  /** 비제어 모드의 처음 열린 항목입니다. 기본은 전부 닫힘입니다. */\n  defaultValue?: string[] | string;\n  /** 열림 상태가 바뀔 때 호출됩니다. */\n  onValueChange?: (value: string[] | string) => void;\n  /** Trigger 를 감쌀 제목 태그의 레벨입니다. */\n  headingLevel?: AccordionHeadingLevel;\n};\n\nconst toArray = (v: string[] | string | undefined): string[] =>\n  v === undefined ? [] : Array.isArray(v) ? v : v === \"\" ? [] : [v];\n\nfunction AccordionRoot({\n  ref,\n  items,\n  type = \"multiple\",\n  variant = \"card\",\n  value,\n  defaultValue,\n  onValueChange,\n  headingLevel = 3,\n  className,\n  children,\n  ...rest\n}: AccordionProps) {\n  const [current, setCurrent] = useControllableState<string[]>({\n    value: value === undefined ? undefined : toArray(value),\n    defaultValue: toArray(defaultValue),\n    onChange: (next) => onValueChange?.(type === \"single\" ? (next[0] ?? \"\") : next),\n  });\n\n  const toggle = (itemValue: string) => {\n    const isOpen = current.includes(itemValue);\n    if (type === \"single\") setCurrent(isOpen ? [] : [itemValue]);\n    else setCurrent(isOpen ? current.filter((v) => v !== itemValue) : [...current, itemValue]);\n  };\n\n  return (\n    <AccordionContext value={{ openValues: current, toggle, headingLevel }}>\n      <div\n        {...rest}\n        ref={ref}\n        className={cn(B, mod(B, \"variant\", variant), mod(B, \"type\", type), className)}\n      >\n        {items?.map((item) => (\n          <AccordionItem key={item.value} value={item.value} disabled={item.disabled}>\n            <AccordionTrigger>{item.title}</AccordionTrigger>\n            <AccordionContent>{item.content}</AccordionContent>\n          </AccordionItem>\n        ))}\n        {children}\n      </div>\n    </AccordionContext>\n  );\n}\n\nexport type AccordionItemProps = HTMLAttributes<HTMLDivElement> & {\n  ref?: Ref<HTMLDivElement>;\n  /** 항목을 구분하는 값입니다. Accordion 의 value 에 들어갑니다. */\n  value: string;\n  /** 항목을 비활성화합니다. */\n  disabled?: boolean;\n};\n\n/** 하나의 접히는 단위. 열림 상태를 data-state 로 가집니다. */\nfunction AccordionItem({ ref, value, disabled = false, className, children, ...rest }: AccordionItemProps) {\n  const { openValues } = useAccordionContext(\"Accordion.Item\");\n  const id = useId();\n  const open = openValues.includes(value);\n  return (\n    <AccordionItemContext value={{ value, open, disabled, triggerId: `${id}-t`, panelId: `${id}-p` }}>\n      <div\n        {...rest}\n        ref={ref}\n        className={cn(`${B}__item`, className)}\n        data-state={open ? \"open\" : \"closed\"}\n        data-disabled={disabled ? \"\" : undefined}\n      >\n        {children}\n      </div>\n    </AccordionItemContext>\n  );\n}\n\nexport type AccordionTriggerProps = ButtonHTMLAttributes<HTMLButtonElement> & {\n  ref?: Ref<HTMLButtonElement>;\n};\n\n/** 제목 태그(headingLevel) 안의 행 전체 버튼. 자식은 atom-accordion__title 로 감싸고 Caret 을 붙입니다. */\nfunction AccordionTrigger({ ref, className, children, onClick, ...rest }: AccordionTriggerProps) {\n  const { toggle, headingLevel } = useAccordionContext(\"Accordion.Trigger\");\n  const item = useAccordionItemContext(\"Accordion.Trigger\");\n  const Heading = `h${headingLevel}` as \"h3\";\n  return (\n    <Heading>\n      <button\n        {...rest}\n        ref={ref}\n        type=\"button\"\n        id={item.triggerId}\n        className={cn(`${B}__trigger`, className)}\n        aria-expanded={item.open}\n        aria-controls={item.panelId}\n        disabled={item.disabled}\n        onClick={(e) => {\n          onClick?.(e);\n          if (!e.defaultPrevented && !item.disabled) toggle(item.value);\n        }}\n      >\n        <span className={`${B}__title`}>{children}</span>\n        <ChevronDownIcon className={`${B}__caret`} />\n      </button>\n    </Heading>\n  );\n}\n\nexport type AccordionContentProps = HTMLAttributes<HTMLDivElement> & {\n  ref?: Ref<HTMLDivElement>;\n};\n\n/** 패널(role=\"region\"). Content 가 높이를 애니메이션하고 Body 가 내용과 여백을 담습니다. */\nfunction AccordionContent({ ref, className, children, ...rest }: AccordionContentProps) {\n  const item = useAccordionItemContext(\"Accordion.Content\");\n  return (\n    <div\n      {...rest}\n      ref={ref}\n      id={item.panelId}\n      role=\"region\"\n      aria-labelledby={item.triggerId}\n      className={cn(`${B}__content`, className)}\n    >\n      <div className={`${B}__body`}>{children}</div>\n    </div>\n  );\n}\n\nexport const Accordion = Object.assign(AccordionRoot, {\n  Item: AccordionItem,\n  Trigger: AccordionTrigger,\n  Content: AccordionContent,\n});\n",
      "type": "registry:ui",
      "target": "components/ui/accordion.tsx"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/accordion.css",
      "content": "/* ============================================================================\n   Element — Accordion\n   ----------------------------------------------------------------------------\n   A vertical stack of disclosure rows. Each item owns its own open state, so\n   whether one panel or many can be open at a time is a script decision, not a\n   CSS one — `--type_*` records which contract the markup follows.\n\n   atom-accordion\n     --variant_{card|flush}                  card is the default\n     --type_{multiple|single}                multiple is the default\n     __item      [data-state=\"open|closed\"]  [data-disabled]\n     __trigger   [disabled]  [aria-disabled=\"true\"]  :hover  :focus-visible\n     __title     __caret\n     __content   __body\n\n   The padding that lines the rows up lives on __trigger and __body, not on the\n   container: the trigger is a full-bleed hit area and its hover fill has to\n   reach the card edge, and the item divider has to span the full width. The\n   container only supplies the surface.\n   ========================================================================== */\n\n.atom-accordion {\n  width: 100%;\n  min-width: 0;\n}\n\n/* --- item ---------------------------------------------------------------- */\n/* Drawn as an inset shadow rather than a border-top so adding or removing an\n   item never leaves a stray rule, and the first row never carries one. */\n.atom-accordion__item + .atom-accordion__item {\n  box-shadow: inset 0 1px 0 var(--atom-border-subtle);\n}\n\n/* --- trigger ------------------------------------------------------------- */\n.atom-accordion__trigger {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--atom-x3);\n  width: 100%;\n  min-height: 48px;\n  padding-block: var(--atom-x3);\n  padding-inline: var(--atom-x4);\n  background: transparent;\n  font-size: 15px;\n  font-weight: 600;\n  line-height: 1.45;\n  text-align: left;\n  color: var(--atom-fg-neutral);\n  cursor: pointer;\n  transition: background-color var(--atom-duration-fast) ease;\n}\n\n.atom-accordion__title {\n  min-width: 0;\n  word-break: keep-all;\n}\n\n.atom-accordion__caret {\n  width: 24px;\n  height: 24px;\n  flex-shrink: 0;\n  color: var(--atom-fg-neutral-muted);\n  transition: transform var(--atom-duration-base) var(--atom-ease-out);\n}\n\n/* --- content ------------------------------------------------------------- */\n/* 0fr → 1fr is the one height transition that works without knowing the\n   content's height. The grid row is what animates; __content clips it.\n\n   The track is `minmax(0, 0fr)`, not the bare `0fr`: `0fr` means\n   `minmax(auto, 0fr)`, and that `auto` floors the track at the item's minimum\n   contribution — which counts __body's padding even though its overflow is\n   hidden. A closed panel kept `--atom-x4` of dead height that way. Pinning the\n   minimum to 0 is what makes a closed panel take up nothing at all.\n\n   __body is `align-self: start` so it holds its natural height through the\n   whole transition instead of stretching to the track. Only the track resizes\n   and __content clips the remainder, so nothing inside the body reflows,\n   squashes or re-wraps while the panel opens or closes.\n\n   `visibility: hidden` is what keeps a closed panel out of the accessibility\n   tree and out of the tab order — a panel collapsed by height alone still\n   takes focus, which is the failure the old `display: none` avoided. The\n   delayed transition holds it visible for the length of the collapse so the\n   animation is not cut short. */\n.atom-accordion__content {\n  display: grid;\n  grid-template-rows: minmax(0, 0fr);\n  overflow: hidden;\n  visibility: hidden;\n  font-size: 15px;\n  line-height: 1.6;\n  color: var(--atom-fg-neutral-muted);\n  transition: grid-template-rows var(--atom-duration-base) var(--atom-ease-out),\n    visibility 0s linear var(--atom-duration-base);\n}\n\n.atom-accordion__body {\n  align-self: start;\n  overflow: hidden;\n  padding-inline: var(--atom-x4);\n  padding-block-end: var(--atom-x4);\n  word-break: keep-all;\n}\n\n/* --- variants ------------------------------------------------------------ */\n/* `card` follows Shadow's rule that permanently visible surfaces are bounded by\n   a border, not an elevation — an accordion sits in the page, it does not float\n   above it. overflow clips the trigger hover fill to the rounded corners. */\n.atom-accordion,\n.atom-accordion--variant_card {\n  overflow: hidden;\n  border-radius: var(--atom-radius-r3);\n  background: var(--atom-bg-layer);\n  box-shadow: inset 0 0 0 1px var(--atom-border-subtle);\n}\n\n/* `flush` has no card edge to terminate the list, so every item carries a\n   bottom rule instead of the `+` divider — otherwise the last panel bleeds\n   into whatever follows it. */\n.atom-accordion--variant_flush {\n  overflow: visible;\n  border-radius: 0;\n  background: transparent;\n  box-shadow: none;\n}\n\n.atom-accordion--variant_flush .atom-accordion__item,\n.atom-accordion--variant_flush .atom-accordion__item + .atom-accordion__item {\n  box-shadow: inset 0 -1px 0 var(--atom-border-subtle);\n}\n\n.atom-accordion--variant_flush .atom-accordion__trigger,\n.atom-accordion--variant_flush .atom-accordion__body {\n  padding-inline: 0;\n}\n\n/* --- type — a contract marker, not a look --------------------------------\n   Both types render identically. What differs is what the script does on a\n   click: `multiple` leaves the other items alone, `single` closes them.\n\n   No CSS rule can enforce that without lying: collapsing a second open panel\n   in `single` markup would leave it visually closed while `aria-expanded` still\n   says open, which is worse than the bug it hides. So the modifier only records\n   the contract — mirrored into a component-local custom property (not a design\n   token) so the script reads the mode from one place instead of matching class\n   names. */\n.atom-accordion--type_multiple {\n  --atom-accordion-type: multiple;\n}\n\n.atom-accordion--type_single {\n  --atom-accordion-type: single;\n}\n\n/* --- states -------------------------------------------------------------- */\n.atom-accordion__item[data-state=\"open\"] .atom-accordion__caret {\n  transform: rotate(180deg);\n}\n\n.atom-accordion__item[data-state=\"open\"] .atom-accordion__content {\n  grid-template-rows: minmax(0, 1fr);\n  visibility: visible;\n  transition: grid-template-rows var(--atom-duration-base) var(--atom-ease-out),\n    visibility 0s;\n}\n\n.atom-accordion__trigger:hover:not([disabled]):not([aria-disabled=\"true\"]) {\n  background: var(--atom-bg-neutral-weak);\n}\n\n/* The trigger fills the row, so the global focus ring's positive offset would\n   land on top of the container's own edge. Pull the ring inside instead. */\n.atom-accordion__trigger:focus-visible {\n  outline: 2px solid var(--atom-fg-brand);\n  outline-offset: -2px;\n}\n\n.atom-accordion__item[data-disabled] .atom-accordion__trigger,\n.atom-accordion__trigger[disabled],\n.atom-accordion__trigger[aria-disabled=\"true\"] {\n  opacity: 0.4;\n  cursor: not-allowed;\n}\n\n/* --- reduced motion ------------------------------------------------------\n   base.css zeroes transition-duration globally but leaves transition-delay\n   alone, and this component's visibility flip rides on a delay — without this\n   block a collapsed panel would stay in the tree for 200ms after closing. */\n@media (prefers-reduced-motion: reduce) {\n  .atom-accordion__caret,\n  .atom-accordion__content,\n  .atom-accordion__item[data-state=\"open\"] .atom-accordion__content {\n    transition: none;\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/accordion.css"
    }
  ],
  "categories": [
    "display"
  ],
  "meta": {
    "version": "0.1.0",
    "kind": "component",
    "status": "stable",
    "specSource": "measured",
    "parts": [
      "Accordion",
      "Accordion.Item",
      "Accordion.Trigger",
      "Accordion.Content"
    ],
    "docs": "poc/design-system/components/accordion.md"
  }
}
