{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-menu",
  "title": "ContextMenu",
  "description": "shadcn/ui(new-york-v4) context-menu의 유니버설(웹+네이티브 한 벌 소스) 포팅. 포팅 판단(원본 대비 차이·이유): radio-group 3종)로 관리하고, 오버레이는 lib/overlay의 AnchoredOverlay가",
  "dependencies": [
    "@unikit-bridge/ui"
  ],
  "registryDependencies": [
    "utils",
    "animations"
  ],
  "files": [
    {
      "path": "registry/ui/context-menu.tsx",
      "content": "// ContextMenu — shadcn/ui（new-york-v4） context-menu.tsx 유니버설 포팅 （사이클 40, T-1d）.\n// 원본: shadcn-ref/ui/context-menu.tsx\n//\n// 포팅 판단（원본 대비 차이이유）:\n// - radix ContextMenu 엔진 제거 — 열림 상태는 React state + context（rootsub\n//   radio-group 3종）로 관리하고, 오버레이는 lib/overlay의 AnchoredOverlay가\n//   대체한다（포털오프셋뷰포트 클램프 포함）. 원본의 Portal 컴포넌트는 API 호환용\n//   투명 래퍼로 남긴다.\n// - 메뉴 진입（핵심 차이）: 트리거 래퍼에 PressableProps.onContextMenu를 연결한다 —\n//   브리지가 웹에서는 onContextMenu（우클릭）, 네이티브에서는 onLongPress（길게\n//   누르기）로 자동 매핑한다（bridge/types.ts §2.1）. 다만 브리지 계약이 이벤트\n//   인자 없는 무인자 콜백이라 preventDefault를 호출할 수 없어 웹에서는 브라우저\n//   기본 우클릭 메뉴가 함께 뜬다 — 브리지 확장 필요（완료 보고에 제기）.\n// - 원본은 메뉴를 우클릭 '좌표'에 띄우지만 브리지 PressableProps에 포인터 좌표\n//   계약이 없어 트리거 '요소' 기준 배치로 근사한다（AnchoredOverlay 앵커링）.\n// - 트리거는 웹에서 <button>으로 렌더된다（원본은 span + asChild） — UA 기본\n//   스타일만 리셋하고 자식（카드 등 임의 콘텐츠）을 세로로 감싼다. asChild\n//   미지원은 dropdown-menu.tsx와 동일.\n// - 서브메뉴（Sub 계열）: 중첩 AnchoredOverlay로 실제 서브메뉴를 구현（1단계\n//   근사가 아님）. 복잡도 판단 — SubTrigger 눌림으로 열고 닫는다（hover 열기는\n//   브리지 계약 밖）, Sub 열림은 비제어 내부 state, ESC는 서브루트가 동시에\n//   닫힌다（document 단위 useDismiss — 근사）.\n// - 아이콘: CheckIcon·ChevronRightIcon — 브리지 lucide 아이콘 프리미티브\n//   （checkbox/radio-group/accordion 관행, lucide — primitives 경유）. CircleIcon은 점 View\n//   （size-2 rounded-full）로 근사.\n// - 항목 onSelect는 무인자 콜백으로 단순화（preventDefault 닫기 방지 미지원）,\n//   선택 시 항상 메뉴가 닫힌다.\n// - 미지원（로드맵）: 등장/퇴장 애니메이션, 화살표키타입어헤드루프 탐색,\n//   포인터 좌표 배치, alignOffsetavoidCollisions. 바깥 포인터 눌림 닫기는\n//   AnchoredOverlay 투명 백드롭이 제공한다（G3 정합 갱신）. 웹에서는\n//   AnchoredOverlay의 포커스 이동\n//   ESC 닫기재배치가, 네이티브에서는 Modal onRequestClose（안드로이드 뒤로가기）\n//   닫기가 동작한다.\n// - 클래스 근사: min-w-［8rem］→min-w-32, max-h-（--radix-）→web:max-h-96,\n//   outline-hidden→web:outline-none, focus 하이라이트→active:+web:hover:+\n//   web:focus-visible:, shadow스크롤류 web: 접두, dark: 제거（dropdown-menu와\n//   동일 기준）.\n// - 접근성: Content=role \"menu\", ItemSubTrigger=\"menuitem\", CheckboxItem\n//   RadioItem aria-checked, TriggerSubTrigger aria-expandedaria-controls（useId）.\n//   RN 한계 — \"menu\"\"menuitem\" role은 네이티브에서 무시（브리지 방침）,\n//   aria-haspopup은 PressableProps 계약에 없어 생략. ContextMenuLabel의 원본\n//   text-foreground 토큰은 유지（dropdown label이 popover-foreground인 것과 대비）.\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 * 렌더할 수 없고 색 상속도 되지 않는다（이중 지정）. 요소 자식은 그대로 둔다\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 ContextMenuContextValue = {\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 ContextMenuContext = createContext<ContextMenuContextValue | null>(null);\n\nfunction useContextMenuContext(component: string): ContextMenuContextValue {\n  const ctx = useContext(ContextMenuContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <ContextMenu> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\ntype ContextMenuSubContextValue = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  triggerRef: PressableRefObject;\n  baseId: string;\n};\n\nconst ContextMenuSubContext = createContext<ContextMenuSubContextValue | null>(\n  null,\n);\n\nfunction useContextMenuSubContext(\n  component: string,\n): ContextMenuSubContextValue {\n  const ctx = useContext(ContextMenuSubContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <ContextMenuSub> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\ntype ContextMenuRadioGroupContextValue = {\n  value: string;\n  onItemSelect: (value: string) => void;\n};\n\n// 기본값 — 그룹 밖 RadioItem도 크래시 없이 동작（radio-group.tsx 관행）.\nconst ContextMenuRadioGroupContext =\n  createContext<ContextMenuRadioGroupContextValue>({\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 unikit-popover-in web:max-h-96 web:overflow-x-hidden web:overflow-y-auto web:shadow-md\";\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 ContextMenuProps = ViewProps & {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\nconst ContextMenu = forwardRef<BridgeViewRef, ContextMenuProps>(\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 stateRef = useRef({ open, isOpen, onOpenChange });\n    stateRef.current = { open, isOpen, onOpenChange };\n    const setOpen = useCallback((next: boolean) => {\n      const state = stateRef.current;\n      if (state.open === undefined) setInternalOpen(next);\n      if (next !== state.isOpen) state.onOpenChange?.(next);\n    }, []);\n    const triggerRef = useRef<BridgePressableRef | null>(null);\n    const baseId = useId();\n    const contextValue = useMemo<ContextMenuContextValue>(\n      () => ({ open: isOpen, setOpen, triggerRef, baseId }),\n      [isOpen, setOpen, baseId],\n    );\n    return (\n      <ContextMenuContext.Provider value={contextValue}>\n        {/* 컨텍스트 트리거는 임의 폭 콘텐츠（카드 등）를 감싸므로 폭 수축 없이 통과 */}\n        <View ref={ref} className={className} {...props}>\n          {children}\n        </View>\n      </ContextMenuContext.Provider>\n    );\n  },\n);\nContextMenu.displayName = \"ContextMenu\";\n\n/** API 호환용 투명 래퍼 — 포털은 AnchoredOverlay 골격이 대체（forceMountcontainer 미지원）. */\nexport type ContextMenuPortalProps = { children?: ReactNode };\n\nfunction ContextMenuPortal({ children }: ContextMenuPortalProps) {\n  return <>{children}</>;\n}\n\nexport type ContextMenuTriggerProps = PressableProps;\n\nconst ContextMenuTrigger = forwardRef<\n  BridgePressableRef,\n  ContextMenuTriggerProps\n>(({ className, children, ...props }, ref) => {\n  const menu = useContextMenuContext(\"ContextMenuTrigger\");\n  return (\n    <Pressable\n      ref={composeRefs(ref, menu.triggerRef)}\n      accessibilityRole=\"button\"\n      aria-expanded={menu.open}\n      aria-controls={`${menu.baseId}-context-menu-content`}\n      // 핵심 진입 — 웹 우클릭 ↔ 네이티브 길게 누르기 자동 매핑（브리지 처리）.\n      // 다시 우클릭/길게 누르면 토글된다（원본은 재배치지만 골격 한계 내 대안）.\n      onContextMenu={() => menu.setOpen(!menu.open)}\n      className={cn(TRIGGER_RESET_CLASSES, className)}\n      {...props}\n    >\n      {wrapTextChildren(children, \"font-sans text-sm text-foreground\")}\n    </Pressable>\n  );\n});\nContextMenuTrigger.displayName = \"ContextMenuTrigger\";\n\nexport type ContextMenuContentProps = ViewProps & {\n  /** 트리거 기준 전개 방향（기본 bottom） — 원본의 포인터 좌표 배치는 미지원 */\n  side?: OverlayPlacement;\n  /** 트리거 변 기준 정렬（기본 start） */\n  align?: OverlayAlign;\n  /** 트리거와의 간격 px（원본 sideOffset, 기본 4） */\n  sideOffset?: number;\n};\n\nconst ContextMenuContent = forwardRef<BridgeViewRef, ContextMenuContentProps>(\n  (\n    {\n      className,\n      side = \"bottom\",\n      align = \"start\",\n      sideOffset = 4,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const menu = useContextMenuContext(\"ContextMenuContent\");\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}-context-menu-content`}\n          className={cn(CARD_CLASSES, className)}\n          {...props}\n        >\n          {children}\n        </View>\n      </AnchoredOverlay>\n    );\n  },\n);\nContextMenuContent.displayName = \"ContextMenuContent\";\n\nexport type ContextMenuGroupProps = ViewProps;\n\nconst ContextMenuGroup = forwardRef<BridgeViewRef, ContextMenuGroupProps>(\n  ({ ...props }, ref) => <View ref={ref} {...props} />,\n);\nContextMenuGroup.displayName = \"ContextMenuGroup\";\n\nexport type ContextMenuItemProps = Omit<PressableProps, \"onPress\"> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n  /** 항목 선택 콜백 — 선택 후 메뉴가 닫힌다（원본 preventDefault 닫기 방지 미지원） */\n  onSelect?: () => void;\n  children?: ReactNode;\n};\n\nconst ContextMenuItem = forwardRef<BridgePressableRef, ContextMenuItemProps>(\n  (\n    {\n      className,\n      inset,\n      variant = \"default\",\n      disabled,\n      onSelect,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const menu = useContextMenuContext(\"ContextMenuItem\");\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);\nContextMenuItem.displayName = \"ContextMenuItem\";\n\nexport type ContextMenuCheckboxItemProps = 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 ContextMenuCheckboxItem = forwardRef<\n  BridgePressableRef,\n  ContextMenuCheckboxItemProps\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 = useContextMenuContext(\"ContextMenuCheckboxItem\");\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 */}\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);\nContextMenuCheckboxItem.displayName = \"ContextMenuCheckboxItem\";\n\nexport type ContextMenuRadioGroupProps = ViewProps & {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n};\n\nconst ContextMenuRadioGroup = forwardRef<\n  BridgeViewRef,\n  ContextMenuRadioGroupProps\n>(({ value, defaultValue = \"\", onValueChange, children, ...props }, ref) => {\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    <ContextMenuRadioGroupContext.Provider value={contextValue}>\n      <View ref={ref} {...props}>\n        {children}\n      </View>\n    </ContextMenuRadioGroupContext.Provider>\n  );\n});\nContextMenuRadioGroup.displayName = \"ContextMenuRadioGroup\";\n\nexport type ContextMenuRadioItemProps = Omit<\n  PressableProps,\n  \"onPress\" | \"aria-checked\"\n> & {\n  /** 이 항목의 값 — 그룹 value와의 일치 여부가 선택 상태가 된다 */\n  value: string;\n  onSelect?: () => void;\n  children?: ReactNode;\n};\n\nconst ContextMenuRadioItem = forwardRef<\n  BridgePressableRef,\n  ContextMenuRadioItemProps\n>(({ className, value, onSelect, disabled, children, ...props }, ref) => {\n  const menu = useContextMenuContext(\"ContextMenuRadioItem\");\n  const group = useContext(ContextMenuRadioGroupContext);\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});\nContextMenuRadioItem.displayName = \"ContextMenuRadioItem\";\n\nexport type ContextMenuLabelProps = ViewProps & {\n  inset?: boolean;\n};\n\nconst ContextMenuLabel = forwardRef<BridgeViewRef, ContextMenuLabelProps>(\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      {/* 원본 label은 text-foreground 토큰을 명시한다（dropdown label과 대비） */}\n      {wrapTextChildren(\n        children,\n        \"font-sans text-sm font-medium text-foreground\",\n      )}\n    </View>\n  ),\n);\nContextMenuLabel.displayName = \"ContextMenuLabel\";\n\nexport type ContextMenuSeparatorProps = ViewProps;\n\nconst ContextMenuSeparator = forwardRef<\n  BridgeViewRef,\n  ContextMenuSeparatorProps\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));\nContextMenuSeparator.displayName = \"ContextMenuSeparator\";\n\nexport type ContextMenuShortcutProps = TextProps;\n\nconst ContextMenuShortcut = forwardRef<\n  BridgeTextRef,\n  ContextMenuShortcutProps\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));\nContextMenuShortcut.displayName = \"ContextMenuShortcut\";\n\nexport type ContextMenuSubProps = { children?: ReactNode };\n\nfunction ContextMenuSub({ children }: ContextMenuSubProps) {\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    <ContextMenuSubContext.Provider value={contextValue}>\n      {children}\n    </ContextMenuSubContext.Provider>\n  );\n}\n\nexport type ContextMenuSubTriggerProps = Omit<PressableProps, \"onPress\"> & {\n  inset?: boolean;\n  children?: ReactNode;\n};\n\nconst ContextMenuSubTrigger = forwardRef<\n  BridgePressableRef,\n  ContextMenuSubTriggerProps\n>(({ className, inset, disabled, children, ...props }, ref) => {\n  const sub = useContextMenuSubContext(\"ContextMenuSubTrigger\");\n  return (\n    <Pressable\n      ref={composeRefs(ref, sub.triggerRef)}\n      accessibilityRole=\"menuitem\"\n      aria-expanded={sub.open}\n      aria-controls={`${sub.baseId}-context-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(ITEM_TEXT_CLASSES, sub.open && \"text-accent-foreground\"),\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});\nContextMenuSubTrigger.displayName = \"ContextMenuSubTrigger\";\n\nexport type ContextMenuSubContentProps = ViewProps & {\n  /** 서브트리거 기준 전개 방향（기본 right — 원본 관행） */\n  side?: OverlayPlacement;\n  align?: OverlayAlign;\n  sideOffset?: number;\n};\n\nconst ContextMenuSubContent = forwardRef<\n  BridgeViewRef,\n  ContextMenuSubContentProps\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 = useContextMenuSubContext(\"ContextMenuSubContent\");\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}-context-menu-sub-content`}\n          className={cn(CARD_CLASSES, \"web:shadow-lg\", className)}\n          {...props}\n        >\n          {children}\n        </View>\n      </AnchoredOverlay>\n    );\n  },\n);\nContextMenuSubContent.displayName = \"ContextMenuSubContent\";\n\nexport {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuCheckboxItem,\n  ContextMenuRadioItem,\n  ContextMenuLabel,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuGroup,\n  ContextMenuPortal,\n  ContextMenuSub,\n  ContextMenuSubContent,\n  ContextMenuSubTrigger,\n  ContextMenuRadioGroup,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/context-menu.tsx"
    }
  ],
  "type": "registry:ui"
}
