{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropdown-menu",
  "title": "DropdownMenu",
  "description": "shadcn/ui(new-york-v4) dropdown-menu의 유니버설(웹+네이티브 한 벌 소스) 포팅. 포팅 판단(원본 대비 차이·이유): radio-group 3종)로 관리하고, 오버레이는 lib/overlay의 AnchoredOverlay",
  "dependencies": [
    "@unikit-bridge/ui"
  ],
  "registryDependencies": [
    "utils",
    "animations"
  ],
  "files": [
    {
      "path": "registry/ui/dropdown-menu.tsx",
      "content": "// DropdownMenu — shadcn/ui（new-york-v4） dropdown-menu.tsx 유니버설 포팅 （사이클 40, T-1d）.\n// 원본: shadcn-ref/ui/dropdown-menu.tsx\n//\n// 포팅 판단（원본 대비 차이이유）:\n// - radix DropdownMenu 엔진 제거 — 열림 상태는 React state + context（rootsub\n//   radio-group 3종）로 관리하고, 오버레이는 lib/overlay의 AnchoredOverlay\n//   （measureInWindow 기반 앵커 팝업 골격）가 대체한다（포털오프셋뷰포트 클램프\n//   포함）. 원본의 Portal 컴포넌트는 API 호환용 투명 래퍼로 남긴다.\n// - 원본의 data-statedata-variantdata-inset［&_svg］ 속성/자손 셀렉터 클래스는\n//   네이티브에 data 속성이 없어 조건부 클래스 문자열로 전개한다.\n// - 서브메뉴（Sub 계열）: 1단계 근사가 아니라 중첩 AnchoredOverlay로 실제 서브메뉴를\n//   구현했다. 복잡도 판단 — SubTrigger 눌림으로 열고 닫는다: radix의 hover로 열기\n//   지연은 포인터 개념이라 브리지 계약（onHoverIn 부재）으로 재현 불가하고, 눌림\n//   방식이 터치에서도 동일하게 동작한다. Sub 열림 상태는 Sub 내부 state（비제어）\n//   로만 관리（원본의 open/onOpenChange 제어 변형 미지원）. ESC는 서브루트가 동시에\n//   닫힌다（useDismiss가 document 단위라 개별 소멸 불가 — 근사）.\n// - 아이콘: CheckIcon·ChevronRightIcon — 브리지 lucide 아이콘 프리미티브\n//   （checkbox/radio-group 관행, lucide — primitives 경유）. CircleIcon은 점 View\n//   （size-2 rounded-full）로 근사.\n// - 트리거: radix asChild 미지원 — 트리거 자체가 브리지 Pressable（웹 <button>）이라\n//   UA 기본 버튼 스타일만 리셋하고 외관은 사용처 className에 맡긴다（Button 등\n//   컴포넌트를 자식으로 넣으면 웹에서 button 중첩이 되니 문자열/텍스트 계열 권장）.\n// - 항목 onSelect는 원본 이벤트 인자（preventDefault로 닫기 방지） 없이 무인자\n//   콜백으로 단순화 — 선택 시 항상 메뉴가 닫힌다.\n// - 미지원（로드맵）: 등장/퇴장 애니메이션（animate-inorigin 변형）, 화살표키\n//   타입어헤드루프 탐색, alignOffsetavoidCollisions 등 포퍼 세부 옵션.\n//   （바깥 포인터 눌림 닫기는 AnchoredOverlay 투명 백드롭이 제공한다 —\n//   G3 리뷰 주석 정합 갱신）\n//   항목은 Pressable이라 웹에서 Tab 포커스엔터 선택은 동작하고 AnchoredOverlay의\n//   포커스 이동（열림 시 첫 포커서블）ESC 닫기스크롤/리사이즈 재배치가 붙는다.\n// - 클래스 근사: min-w-［8rem］→min-w-32（표준 계단）, max-h-（--radix-available-\n//   height）→web:max-h-96, outline-hidden→web:outline-none, radix data-［state=open］\n//   하이라이트→active:+web:hover:+web:focus-visible: 조건부 전개, shadow-md\n//   스크롤류는 web: 접두, dark: 제거, 커서/선택 계열은 웹 전용 개념이라 web: 접두.\n// - 접근성: Content=role \"menu\", ItemSubTrigger=\"menuitem\", CheckboxItem\n//   RadioItem은 aria-checked, TriggerSubTrigger는 aria-expandedaria-controls\n//   （useId 토대로 Content nativeID와 결정적 연결）. RN 한계 — \"menu\"\"menuitem\" role\n//   은 RN accessibilityRole 카탈로그에 없어 네이티브에서 무시된다（브리지 문서화\n//   방침, 웹 스크린리더만 이득）, aria-haspopup은 브리지 PressableProps 계약에\n//   없어 생략, 키보드 탐색은 위 미지원 항목 참조.\nimport {\n  Children,\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ForwardedRef,\n  type ReactNode,\n} from \"react\";\nimport {\n  CheckIcon,\n  ChevronRightIcon,\n  Pressable,\n  Text,\n  View,\n  type BridgePressableRef,\n  type BridgeTextRef,\n  type BridgeViewRef,\n  type PressableProps,\n  type TextProps,\n  type ViewProps,\n} from \"@unikit-bridge/ui/primitives\";\n\nimport {\n  AnchoredOverlay,\n  type OverlayAlign,\n  type OverlayPlacement,\n} from \"../lib/overlay\";\nimport { cn } from \"@/lib/utils\";\n\n// ---------------------------------------------------------------------------\n// 내부 유틸\n// ---------------------------------------------------------------------------\n\ntype PressableRefObject = { current: BridgePressableRef | null };\n\n/** 트리거의 외부 forwardRef와 앵커 측정용 내부 ref를 한 콜백으로 합성한다. */\nfunction composeRefs(\n  ...refs: Array<ForwardedRef<BridgePressableRef> | PressableRefObject>\n) {\n  return (node: BridgePressableRef | null) => {\n    for (const ref of refs) {\n      if (!ref) continue;\n      if (typeof ref === \"function\") ref(node);\n      else ref.current = node;\n    }\n  };\n}\n\n/**\n * 자식 중 문자열/숫자를 내부 Text로 감싼다 — 네이티브는 Text 없이 문자열을\n * 렌더할 수 없고 색 상속도 되지 않는다（이중 지정）. 요소 자식（Shortcut 등）은\n * 그대로 둔다（table.tsx 관행 — 사용자 정의 콘텐츠는 직접 Text를 넣는다）.\n */\nfunction wrapTextChildren(children: ReactNode, textClassName: string) {\n  return Children.map(children, (child) =>\n    typeof child === \"string\" || typeof child === \"number\" ? (\n      <Text className={textClassName}>{child}</Text>\n    ) : (\n      child\n    ),\n  );\n}\n\n// ---------------------------------------------------------------------------\n// 상태 context — root（열림）sub（서브메뉴）radio-group（단일 선택）\n// ---------------------------------------------------------------------------\n\ntype DropdownMenuContextValue = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  /** 콘텐츠（AnchoredOverlay）가 앵커로 측정할 트리거 요소 ref */\n  triggerRef: PressableRefObject;\n  /** 트리거 aria-controls ↔ 콘텐츠 nativeID 연결용 토대 ID */\n  baseId: string;\n};\n\nconst DropdownMenuContext = createContext<DropdownMenuContextValue | null>(null);\n\nfunction useDropdownMenuContext(component: string): DropdownMenuContextValue {\n  const ctx = useContext(DropdownMenuContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <DropdownMenu> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\ntype DropdownMenuSubContextValue = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  triggerRef: PressableRefObject;\n  baseId: string;\n};\n\nconst DropdownMenuSubContext = createContext<DropdownMenuSubContextValue | null>(\n  null,\n);\n\nfunction useDropdownMenuSubContext(\n  component: string,\n): DropdownMenuSubContextValue {\n  const ctx = useContext(DropdownMenuSubContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <DropdownMenuSub> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\ntype DropdownMenuRadioGroupContextValue = {\n  value: string;\n  onItemSelect: (value: string) => void;\n};\n\n// 기본값 — 그룹 밖 RadioItem도 크래시 없이 동작（radio-group.tsx 관행）.\nconst DropdownMenuRadioGroupContext =\n  createContext<DropdownMenuRadioGroupContextValue>({\n    value: \"\",\n    onItemSelect: () => {},\n  });\n\n// ---------------------------------------------------------------------------\n// 공통 클래스 상수\n// ---------------------------------------------------------------------------\n\n// 메뉴 카드 — 원본 콘텐츠 토큰 + 골격 요구 pointer-events-auto（§규칙 6）.\nconst CARD_CLASSES =\n  \"pointer-events-auto z-50 min-w-32 rounded-md border border-border bg-popover p-1 text-popover-foreground web:max-h-96 web:overflow-x-hidden web:overflow-y-auto web:shadow-md unikit-popover-in\";\n\n// 일반 항목 — 기본 상호작용 및 커서/선택 방지/포커스 아웃라인.\nconst ITEM_CLASSES =\n  \"relative flex-row items-center gap-2 rounded-sm px-2 py-1.5 web:cursor-default web:select-none web:outline-none\";\n\n// 일반 항목 하이라이트 — radix 하이라이트（focus:bg-accent focus:text-accent-foreground）를\n// active（네이티브 눌림）+hover/focus-visible로. 전경색 전환도 원본과 같이 함께.\nconst ITEM_HIGHLIGHT_CLASSES =\n  \"active:bg-accent active:text-accent-foreground web:hover:bg-accent web:hover:text-accent-foreground web:focus-visible:bg-accent web:focus-visible:text-accent-foreground\";\n\n// 표시자（체크/라디오） 항목 — 왼쪽 pl-8 표시자 슬롯（원본 py-1.5 pr-2 pl-8）.\nconst INDICATED_ITEM_CLASSES =\n  \"relative flex-row items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 active:bg-accent active:text-accent-foreground web:cursor-default web:select-none web:outline-none web:hover:bg-accent web:hover:text-accent-foreground web:focus-visible:bg-accent web:focus-visible:text-accent-foreground\";\n\n// 항목 텍스트 — 네이티브 색 상속 불가로 항목마다 명시（이중 지정）. 하이라이트\n// 전경색도 이중 지정해야 웹에서 Text 고유 클래스가 컨테이너 전환을 덮지 않는다.\nconst ITEM_TEXT_CLASSES =\n  \"flex-1 font-sans text-sm text-popover-foreground active:text-accent-foreground web:hover:text-accent-foreground web:focus-visible:text-accent-foreground\";\n\n// 표시자 슬롯 — 원본 span（pointer-events-none absolute left-2 size-3.5）.\nconst INDICATOR_SLOT_CLASSES =\n  \"absolute left-2 size-3.5 items-center justify-center web:pointer-events-none\";\n\n// 트리거 리셋 — 포커스 아웃라인 기본 처리(소비자 클래스 우선 보존).\nconst TRIGGER_RESET_CLASSES = \"web:outline-none\";\n\n// ---------------------------------------------------------------------------\n// 컴포넌트\n// ---------------------------------------------------------------------------\n\nexport type DropdownMenuProps = ViewProps & {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\nconst DropdownMenu = forwardRef<BridgeViewRef, DropdownMenuProps>(\n  (\n    { open, defaultOpen = false, onOpenChange, className, children, ...props },\n    ref,\n  ) => {\n    // 비제어 상태 — 제어（open 지정）면 무시된다.\n    const [internalOpen, setInternalOpen] = useState(defaultOpen);\n    const isOpen = open !== undefined ? open : internalOpen;\n    const isOpenRef = useRef(isOpen);\n    isOpenRef.current = isOpen;\n    const setOpen = useCallback(\n      (next: boolean) => {\n        if (open === undefined) setInternalOpen(next);\n        if (next !== isOpenRef.current) onOpenChange?.(next);\n      },\n      [open, onOpenChange],\n    );\n    const triggerRef = useRef<BridgePressableRef | null>(null);\n    const baseId = useId();\n    const contextValue = useMemo(\n      () => ({ open: isOpen, setOpen, triggerRef, baseId }),\n      [isOpen, setOpen, baseId],\n    );\n    return (\n      <DropdownMenuContext.Provider value={contextValue}>\n        {/* w-fit 미지원 — kbd.tsx 관행의 self-start（교차축 시작 정렬）로 트리거\n            폭 수축. 풀폭 트리거는 사용처 className(self-stretch)으로. */}\n        <View ref={ref} className={cn(\"self-start\", className)} {...props}>\n          {children}\n        </View>\n      </DropdownMenuContext.Provider>\n    );\n  },\n);\nDropdownMenu.displayName = \"DropdownMenu\";\n\n/** API 호환용 투명 래퍼 — 포털은 AnchoredOverlay 골격이 대체（forceMountcontainer 미지원）. */\nexport type DropdownMenuPortalProps = { children?: ReactNode };\n\nfunction DropdownMenuPortal({ children }: DropdownMenuPortalProps) {\n  return <>{children}</>;\n}\n\nexport type DropdownMenuTriggerProps = PressableProps;\n\nconst DropdownMenuTrigger = forwardRef<\n  BridgePressableRef,\n  DropdownMenuTriggerProps\n>(\n  (\n    { className, children, disabled, onPress, ...props },\n    ref,\n  ) => {\n    const menu = useDropdownMenuContext(\"DropdownMenuTrigger\");\n    const contentId = `${menu.baseId}-dropdown-menu-content`;\n    return (\n      <Pressable\n        ref={composeRefs(ref, menu.triggerRef)}\n        accessibilityRole=\"button\"\n        aria-expanded={menu.open}\n        aria-controls={contentId}\n        disabled={disabled}\n        onPress={() => {\n          menu.setOpen(!menu.open);\n          onPress?.();\n        }}\n        className={cn(TRIGGER_RESET_CLASSES, className)}\n        {...props}\n      >\n        {wrapTextChildren(children, \"font-sans text-sm text-foreground\")}\n      </Pressable>\n    );\n  },\n);\nDropdownMenuTrigger.displayName = \"DropdownMenuTrigger\";\n\nexport type DropdownMenuContentProps = ViewProps & {\n  /** 트리거 기준 전개 방향（기본 bottom） */\n  side?: OverlayPlacement;\n  /** 트리거 변 기준 정렬（기본 start） */\n  align?: OverlayAlign;\n  /** 트리거와의 간격 px（원본 sideOffset, 기본 4） */\n  sideOffset?: number;\n};\n\nconst DropdownMenuContent = forwardRef<\n  BridgeViewRef,\n  DropdownMenuContentProps\n>(\n  (\n    {\n      className,\n      side = \"bottom\",\n      align = \"start\",\n      sideOffset = 4,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const menu = useDropdownMenuContext(\"DropdownMenuContent\");\n    return (\n      <AnchoredOverlay\n        visible={menu.open}\n        onClose={() => menu.setOpen(false)}\n        anchorRef={menu.triggerRef}\n        placement={side}\n        align={align}\n        offset={sideOffset}\n        className=\"z-50\"\n        accessibilityRole=\"menu\"\n      >\n        <View\n          ref={ref}\n          nativeID={`${menu.baseId}-dropdown-menu-content`}\n          className={cn(CARD_CLASSES, className)}\n          {...props}\n        >\n          {children}\n        </View>\n      </AnchoredOverlay>\n    );\n  },\n);\nDropdownMenuContent.displayName = \"DropdownMenuContent\";\n\nexport type DropdownMenuGroupProps = ViewProps;\n\nconst DropdownMenuGroup = forwardRef<BridgeViewRef, DropdownMenuGroupProps>(\n  ({ ...props }, ref) => <View ref={ref} {...props} />,\n);\nDropdownMenuGroup.displayName = \"DropdownMenuGroup\";\n\nexport type DropdownMenuItemProps = Omit<PressableProps, \"onPress\"> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n  /** 항목 선택 콜백 — 선택 후 메뉴가 닫힌다（원본 preventDefault 닫기 방지 미지원） */\n  onSelect?: () => void;\n  children?: ReactNode;\n};\n\nconst DropdownMenuItem = forwardRef<BridgePressableRef, DropdownMenuItemProps>(\n  (\n    {\n      className,\n      inset,\n      variant = \"default\",\n      disabled,\n      onSelect,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const menu = useDropdownMenuContext(\"DropdownMenuItem\");\n    const destructive = variant === \"destructive\";\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"menuitem\"\n        disabled={disabled}\n        onPress={() => {\n          onSelect?.();\n          menu.setOpen(false);\n        }}\n        className={cn(\n          ITEM_CLASSES,\n          destructive\n            ? \"active:bg-destructive/10 web:hover:bg-destructive/10 web:focus-visible:bg-destructive/10\"\n            : ITEM_HIGHLIGHT_CLASSES,\n          inset && \"pl-8\",\n          disabled && \"web:pointer-events-none opacity-50\",\n          className,\n        )}\n        {...props}\n      >\n        {wrapTextChildren(\n          children,\n          // 파탈 항목은 하이라이트 중에도 원본처럼 destructive 전경을 유지한다.\n          cn(\n            ITEM_TEXT_CLASSES,\n            destructive &&\n              \"text-destructive active:text-destructive web:hover:text-destructive web:focus-visible:text-destructive\",\n          ),\n        )}\n      </Pressable>\n    );\n  },\n);\nDropdownMenuItem.displayName = \"DropdownMenuItem\";\n\nexport type DropdownMenuCheckboxItemProps = Omit<\n  PressableProps,\n  \"onPress\" | \"aria-checked\"\n> & {\n  /** 제어형 체크 상태（미지정 시 내부 state） */\n  checked?: boolean;\n  defaultChecked?: boolean;\n  onCheckedChange?: (checked: boolean) => void;\n  onSelect?: () => void;\n  children?: ReactNode;\n};\n\nconst DropdownMenuCheckboxItem = forwardRef<\n  BridgePressableRef,\n  DropdownMenuCheckboxItemProps\n>(\n  (\n    {\n      className,\n      checked,\n      defaultChecked = false,\n      onCheckedChange,\n      onSelect,\n      disabled,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const menu = useDropdownMenuContext(\"DropdownMenuCheckboxItem\");\n    // 제어/비제어 통합 — checkbox.tsx와 동일 구조.\n    const [uncontrolled, setUncontrolled] = useState(defaultChecked);\n    const isChecked = checked !== undefined ? checked : uncontrolled;\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"menuitem\"\n        aria-checked={isChecked}\n        disabled={disabled}\n        onPress={() => {\n          const next = !isChecked;\n          if (checked === undefined) setUncontrolled(next);\n          onCheckedChange?.(next);\n          onSelect?.();\n          menu.setOpen(false);\n        }}\n        className={cn(\n          INDICATED_ITEM_CLASSES,\n          disabled && \"web:pointer-events-none opacity-50\",\n          className,\n        )}\n        {...props}\n      >\n        {/* 표시자 — 원본 ItemIndicator + CheckIcon（원본: 슬롯 size-3.5 안의 size-4 아이콘） */}\n        <View aria-hidden className={INDICATOR_SLOT_CLASSES}>\n          {isChecked ? (\n            <CheckIcon className=\"size-4 text-foreground\" />\n          ) : null}\n        </View>\n        {wrapTextChildren(children, ITEM_TEXT_CLASSES)}\n      </Pressable>\n    );\n  },\n);\nDropdownMenuCheckboxItem.displayName = \"DropdownMenuCheckboxItem\";\n\nexport type DropdownMenuRadioGroupProps = ViewProps & {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n};\n\nconst DropdownMenuRadioGroup = forwardRef<\n  BridgeViewRef,\n  DropdownMenuRadioGroupProps\n>(\n  (\n    { value, defaultValue = \"\", onValueChange, children, ...props },\n    ref,\n  ) => {\n    // 제어/비제어 통합 — 라디오는 단일 선택이라 문자열 state면 충분하다.\n    const [uncontrolled, setUncontrolled] = useState(defaultValue);\n    const selected = value !== undefined ? value : uncontrolled;\n    const onItemSelect = useCallback(\n      (itemValue: string) => {\n        if (value === undefined) setUncontrolled(itemValue);\n        onValueChange?.(itemValue);\n      },\n      [value, onValueChange],\n    );\n    const contextValue = useMemo(\n      () => ({ value: selected, onItemSelect }),\n      [selected, onItemSelect],\n    );\n    return (\n      <DropdownMenuRadioGroupContext.Provider value={contextValue}>\n        <View ref={ref} {...props}>\n          {children}\n        </View>\n      </DropdownMenuRadioGroupContext.Provider>\n    );\n  },\n);\nDropdownMenuRadioGroup.displayName = \"DropdownMenuRadioGroup\";\n\nexport type DropdownMenuRadioItemProps = Omit<\n  PressableProps,\n  \"onPress\" | \"aria-checked\"\n> & {\n  /** 이 항목의 값 — 그룹 value와의 일치 여부가 선택 상태가 된다 */\n  value: string;\n  onSelect?: () => void;\n  children?: ReactNode;\n};\n\nconst DropdownMenuRadioItem = forwardRef<\n  BridgePressableRef,\n  DropdownMenuRadioItemProps\n>(({ className, value, onSelect, disabled, children, ...props }, ref) => {\n  const menu = useDropdownMenuContext(\"DropdownMenuRadioItem\");\n  const group = useContext(DropdownMenuRadioGroupContext);\n  const isChecked = group.value === value;\n  return (\n    <Pressable\n      ref={ref}\n      accessibilityRole=\"menuitem\"\n      aria-checked={isChecked}\n      disabled={disabled}\n      onPress={() => {\n        group.onItemSelect(value);\n        onSelect?.();\n        menu.setOpen(false);\n      }}\n      className={cn(\n        INDICATED_ITEM_CLASSES,\n        disabled && \"web:pointer-events-none opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      {/* 표시자 — 원본 ItemIndicator + CircleIcon（fill-current size-2） 대신 점\n          View(radio-group.tsx 관행). 배경이 popover라 foreground 점. */}\n      <View aria-hidden className={INDICATOR_SLOT_CLASSES}>\n        {isChecked ? (\n          <View className=\"size-2 rounded-full bg-foreground\" />\n        ) : null}\n      </View>\n      {wrapTextChildren(children, ITEM_TEXT_CLASSES)}\n    </Pressable>\n  );\n});\nDropdownMenuRadioItem.displayName = \"DropdownMenuRadioItem\";\n\nexport type DropdownMenuLabelProps = ViewProps & {\n  inset?: boolean;\n};\n\nconst DropdownMenuLabel = forwardRef<BridgeViewRef, DropdownMenuLabelProps>(\n  ({ className, inset, children, ...props }, ref) => (\n    <View\n      ref={ref}\n      className={cn(\"px-2 py-1.5\", inset && \"pl-8\", className)}\n      {...props}\n    >\n      {wrapTextChildren(\n        children,\n        \"font-sans text-sm font-medium text-popover-foreground\",\n      )}\n    </View>\n  ),\n);\nDropdownMenuLabel.displayName = \"DropdownMenuLabel\";\n\nexport type DropdownMenuSeparatorProps = ViewProps;\n\nconst DropdownMenuSeparator = forwardRef<\n  BridgeViewRef,\n  DropdownMenuSeparatorProps\n>(({ className, ...props }, ref) => (\n  <View\n    ref={ref}\n    accessibilityRole=\"separator\"\n    className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n    {...props}\n  />\n));\nDropdownMenuSeparator.displayName = \"DropdownMenuSeparator\";\n\nexport type DropdownMenuShortcutProps = TextProps;\n\nconst DropdownMenuShortcut = forwardRef<\n  BridgeTextRef,\n  DropdownMenuShortcutProps\n>(({ className, ...props }, ref) => (\n  // 항목 flex-row의 직계 자식이 되므로 ml-auto가 양쪽 플랫폼에서 동작한다.\n  <Text\n    ref={ref}\n    className={cn(\n      \"ml-auto font-sans text-xs tracking-widest text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\";\n\nexport type DropdownMenuSubProps = { children?: ReactNode };\n\nfunction DropdownMenuSub({ children }: DropdownMenuSubProps) {\n  // 서브 열림은 비제어 — 눌림 토글로만 조작（원본 open/onOpenChange 제어 미지원）.\n  const [open, setOpen] = useState(false);\n  const triggerRef = useRef<BridgePressableRef | null>(null);\n  const baseId = useId();\n  const contextValue = useMemo(\n    () => ({ open, setOpen, triggerRef, baseId }),\n    [open, baseId],\n  );\n  return (\n    <DropdownMenuSubContext.Provider value={contextValue}>\n      {children}\n    </DropdownMenuSubContext.Provider>\n  );\n}\n\nexport type DropdownMenuSubTriggerProps = Omit<PressableProps, \"onPress\"> & {\n  inset?: boolean;\n  children?: ReactNode;\n};\n\nconst DropdownMenuSubTrigger = forwardRef<\n  BridgePressableRef,\n  DropdownMenuSubTriggerProps\n>(\n  (\n    { className, inset, disabled, children, ...props },\n    ref,\n  ) => {\n    const sub = useDropdownMenuSubContext(\"DropdownMenuSubTrigger\");\n    return (\n      <Pressable\n        ref={composeRefs(ref, sub.triggerRef)}\n        accessibilityRole=\"menuitem\"\n        aria-expanded={sub.open}\n        aria-controls={`${sub.baseId}-dropdown-menu-sub-content`}\n        disabled={disabled}\n        onPress={() => sub.setOpen(!sub.open)}\n        className={cn(\n          ITEM_CLASSES,\n          ITEM_HIGHLIGHT_CLASSES,\n          inset && \"pl-8\",\n          // 원본 data-［state=open］:bg-accent → 조건부 클래스\n          sub.open && \"bg-accent active:bg-accent\",\n          disabled && \"web:pointer-events-none opacity-50\",\n          className,\n        )}\n        {...props}\n      >\n        {wrapTextChildren(\n          children,\n          cn(\n            ITEM_TEXT_CLASSES,\n            sub.open && \"text-accent-foreground\",\n          ),\n        )}\n        <ChevronRightIcon\n          className={cn(\n            \"ml-auto size-4\",\n            sub.open ? \"text-accent-foreground\" : \"text-muted-foreground\",\n          )}\n        />\n      </Pressable>\n    );\n  },\n);\nDropdownMenuSubTrigger.displayName = \"DropdownMenuSubTrigger\";\n\nexport type DropdownMenuSubContentProps = ViewProps & {\n  /** 서브트리거 기준 전개 방향（기본 right — 원본 관행） */\n  side?: OverlayPlacement;\n  align?: OverlayAlign;\n  sideOffset?: number;\n};\n\nconst DropdownMenuSubContent = forwardRef<\n  BridgeViewRef,\n  DropdownMenuSubContentProps\n>(\n  (\n    {\n      className,\n      side = \"right\",\n      align = \"start\",\n      sideOffset = 4,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const sub = useDropdownMenuSubContext(\"DropdownMenuSubContent\");\n    return (\n      // 중첩 AnchoredOverlay — 서브트리거에 앵커링된 2단 레벨 메뉴.\n      <AnchoredOverlay\n        visible={sub.open}\n        onClose={() => sub.setOpen(false)}\n        anchorRef={sub.triggerRef}\n        placement={side}\n        align={align}\n        offset={sideOffset}\n        className=\"z-50\"\n        accessibilityRole=\"menu\"\n      >\n        <View\n          ref={ref}\n          nativeID={`${sub.baseId}-dropdown-menu-sub-content`}\n          // 원본 서브 콘텐츠: overflow-hidden·shadow-lg（본체는 overflow-y-auto·shadow-md와 구별）\n          className={cn(\n            \"pointer-events-auto z-50 min-w-32 overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground web:shadow-lg unikit-popover-in\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n        </View>\n      </AnchoredOverlay>\n    );\n  },\n);\nDropdownMenuSubContent.displayName = \"DropdownMenuSubContent\";\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/dropdown-menu.tsx"
    }
  ],
  "type": "registry:ui"
}
