{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "accordion",
  "title": "Accordion",
  "description": "유니버설 아코디언 — single/multiple 모드, ⌄/⌃ 글리프 전환, trigger aria-expanded. 콘텐츠는 펼침 시 마운트.",
  "dependencies": [
    "@unikit-bridge/ui",
    "class-variance-authority",
    "clsx",
    "tailwind-merge",
    "tailwindcss"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/accordion.tsx",
      "content": "// Accordion — shadcn/ui（new-york-v4） accordion.tsx 유니버설 포팅 （사이클 30, T-3b）.\n// 원본: shadcn-ref/ui/accordion.tsx — radix Accordion 엔진을 제거하고\n// 전개 상태는 React state + context로 직접 관리한다（collapsible.tsx의 다중 항목\n// 확장 구조）. 원본의 data-state·셀렉터 클래스는 조건부 클래스·아이콘 회전으로 전개.\n//\n// 포팅 판단（원본 대비 차이）:\n// - ChevronDownIcon은 브리지 lucide 아이콘 프리미티브를 쓰고 open 시 rotate-180\n//   회전（원본 ［&［data-state=open］>svg］:rotate-180의 조건부 클래스 전개）.\n// - 트리거 focus 링은 원본 규격（ring-3 · ring-ring/50 · 무오프셋 ·\n//   focus-visible:border-ring）을 web: 접두로 준수한다（사이클 72）.\n// - 항목 구분선의 마지막 항목 제외（last 변형）는 위치 셀렉터라 네이티브 재현 불가\n//   — 제거. 마지막 항목 하단선 제거는 사용처 className 오버라이드로 한다.\n// - 콘텐츠 전이 애니메이션 미지원 — value 일치 시에만 마운트（즉시 전환）.\n//   원본의 이중 뷰（애니메이션용 바깥쪽 + 패딩 안쪽）는 애니메이션 상실로 한 벌로\n//   통합하고 사용처 className은 같은 패딩 토큰 위에 병합된다.\n// - single 모드의 collapsible은 기본 true（shadcn 관용 — 원본 radix 기본값은 false）.\n// - aria-controls ↔ 콘텐츠 nativeID를 루트 useId 토대로 결정적으로 연결한다.\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport {\n  animateLayout,\n  ChevronDownIcon,\n  Pressable,\n  Text,\n  View,\n  type BridgePressableRef,\n  type BridgeViewRef,\n  type PressableProps,\n  type ViewProps,\n} from \"@unikit-bridge/ui/primitives\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype AccordionContextValue = {\n  type: \"single\" | \"multiple\";\n  collapsible: boolean;\n  openItems: string[];\n  toggleItem: (value: string) => void;\n  /** 트리거 aria-controls ↔ 콘텐츠 nativeID 연결용 토대 ID */\n  baseId: string;\n};\n\nconst AccordionContext = createContext<AccordionContextValue | null>(null);\n\nfunction useAccordionContext(component: string): AccordionContextValue {\n  const ctx = useContext(AccordionContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <Accordion> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\ntype AccordionItemContextValue = {\n  value: string;\n  open: boolean;\n  toggle: () => void;\n  contentId: string;\n};\n\nconst AccordionItemContext = createContext<AccordionItemContextValue | null>(\n  null,\n);\n\nfunction useAccordionItemContext(\n  component: string,\n): AccordionItemContextValue {\n  const ctx = useContext(AccordionItemContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <AccordionItem> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\n// 콘텐츠 패널 ID — value의 공백은 ID에 쓸 수 없어 치환.\nfunction contentIdOf(baseId: string, value: string) {\n  return `${baseId}-accordion-content-${value.replace(/\\s+/g, \"-\")}`;\n}\n\nfunction toItems(value: string | string[] | undefined): string[] {\n  if (value === undefined) return [];\n  return Array.isArray(value) ? value : [value];\n}\n\nexport type AccordionProps = ViewProps & {\n  type?: \"single\" | \"multiple\";\n  /** single 모드에서 펼친 항목 다시 접기 허용 여부（기본 true — shadcn 관용） */\n  collapsible?: boolean;\n  /** single 모드에서는 문자열 1개, multiple 모드에서는 펼친 value 배열 */\n  value?: string | string[];\n  defaultValue?: string | string[];\n  onValueChange?: (value: string | string[]) => void;\n};\n\nconst Accordion = forwardRef<BridgeViewRef, AccordionProps>(\n  (\n    { type = \"single\", collapsible = true, value, defaultValue, onValueChange, children, ...props },\n    ref,\n  ) => {\n    // 비제어 상태 — 제어（value 지정）면 무시된다.\n    const [internalItems, setInternalItems] = useState(() =>\n      toItems(defaultValue),\n    );\n    const openItems = value !== undefined ? toItems(value) : internalItems;\n    const openItemsRef = useRef(openItems);\n    openItemsRef.current = openItems;\n    const toggleItem = useCallback(\n      (itemValue: string) => {\n        animateLayout();\n        const currentItems = openItemsRef.current;\n        const isOpen = currentItems.includes(itemValue);\n        let next: string[];\n        if (isOpen) {\n          // single 비접이형（collapsible=false）은 마지막 펼침 항목을 유지한다.\n          if (type === \"single\" && !collapsible) return;\n          next = currentItems.filter((v) => v !== itemValue);\n        } else {\n          next =\n            type === \"single\"\n              ? [itemValue]\n              : [...currentItems, itemValue];\n        }\n        if (value === undefined) setInternalItems(next);\n        if (type === \"single\") onValueChange?.(next[0] ?? \"\");\n        else onValueChange?.(next);\n      },\n      [type, collapsible, value, onValueChange],\n    );\n    const baseId = useId();\n    const contextValue = useMemo(\n      () => ({ type, collapsible, openItems, toggleItem, baseId }),\n      [type, collapsible, openItems, toggleItem, baseId],\n    );\n    return (\n      <AccordionContext.Provider value={contextValue}>\n        <View ref={ref} {...props}>\n          {children}\n        </View>\n      </AccordionContext.Provider>\n    );\n  },\n);\nAccordion.displayName = \"Accordion\";\n\nexport type AccordionItemProps = ViewProps & {\n  value: string;\n};\n\nconst AccordionItem = forwardRef<BridgeViewRef, AccordionItemProps>(\n  ({ value, className, children, ...props }, ref) => {\n    const accordion = useAccordionContext(\"AccordionItem\");\n    const open = accordion.openItems.includes(value);\n    const toggle = useCallback(\n      () => accordion.toggleItem(value),\n      [accordion.toggleItem, value],\n    );\n    const contentId = contentIdOf(accordion.baseId, value);\n    const itemContextValue = useMemo(\n      () => ({ value, open, toggle, contentId }),\n      [value, open, toggle, contentId],\n    );\n    return (\n      <AccordionItemContext.Provider value={itemContextValue}>\n        <View\n          ref={ref}\n          className={cn(\"border-b border-border\", className)}\n          {...props}\n        >\n          {children}\n        </View>\n      </AccordionItemContext.Provider>\n    );\n  },\n);\nAccordionItem.displayName = \"AccordionItem\";\n\nexport type AccordionTriggerProps = PressableProps & {\n  // 함수형 children은 내부 Text 래핑 구조와 양립하지 않아 문자열 children 권장.\n  children?: ReactNode;\n};\n\nconst AccordionTrigger = forwardRef<BridgePressableRef, AccordionTriggerProps>(\n  ({ className, children, disabled, onPress, ...props }, ref) => {\n    const item = useAccordionItemContext(\"AccordionTrigger\");\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"button\"\n        aria-expanded={item.open}\n        aria-controls={item.contentId}\n        disabled={disabled}\n        onPress={() => {\n          item.toggle();\n          onPress?.();\n        }}\n        className={cn(\n          \"flex-row items-center justify-between gap-4 rounded-md py-4 web:transition-all\",\n          \"web:focus-visible:border-ring web:focus-visible:ring-3 web:focus-visible:ring-ring/50\",\n          disabled && \"web:pointer-events-none opacity-50\",\n          className,\n        )}\n        {...props}\n      >\n        {/* 이중 지정: 전경색·밑줄은 내부 Text에（네이티브는 색 상속 불가） */}\n        <Text className=\"flex-1 font-sans text-left text-sm font-medium text-foreground web:hover:underline\">\n          {children}\n        </Text>\n        <ChevronDownIcon\n          className={cn(\n            \"size-4 shrink-0 text-muted-foreground transition-transform duration-200\",\n            item.open && \"rotate-180\",\n          )}\n        />\n      </Pressable>\n    );\n  },\n);\nAccordionTrigger.displayName = \"AccordionTrigger\";\n\nexport type AccordionContentProps = ViewProps;\n\nconst AccordionContent = forwardRef<BridgeViewRef, AccordionContentProps>(\n  ({ className, children, nativeID, ...props }, ref) => {\n    const item = useAccordionItemContext(\"AccordionContent\");\n    // 펼침 항목만 렌더 — 마운트/언마운트 방식（애니메이션 없음）.\n    if (!item.open) return null;\n    return (\n      <View\n        ref={ref}\n        nativeID={nativeID ?? item.contentId}\n        className={cn(\"overflow-hidden pb-4 pt-0\", className)}\n        {...props}\n      >\n        {children}\n      </View>\n    );\n  },\n);\nAccordionContent.displayName = \"AccordionContent\";\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n",
      "type": "registry:ui",
      "target": "components/unikit/accordion.tsx"
    }
  ],
  "type": "registry:ui"
}
