{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "alert-dialog",
  "title": "AlertDialog",
  "description": "shadcn/ui(new-york-v4) alert-dialog의 유니버설(웹+네이티브 한 벌 소스) 포팅. 상태는 React state + context로 직접 관리한다. 포털·백드롭·포커스 관리·ESC 닫기· 스크롤 잠금은 lib/overlay의 ModalOverlay가 담당한다(dialog.tsx 형제 포팅).",
  "dependencies": [
    "@unikit-bridge/ui",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils",
    "animations"
  ],
  "files": [
    {
      "path": "registry/ui/alert-dialog.tsx",
      "content": "// AlertDialog — shadcn/ui（new-york-v4） alert-dialog.tsx 유니버설 포팅 （사이클 40, T-1a）.\n// 원본: shadcn-ref/ui/alert-dialog.tsx — radix AlertDialog 엔진을 제거하고 열림\n// 상태는 React state + context로 직접 관리한다. 포털백드롭포커스 관리ESC 닫기\n// 스크롤 잠금은 lib/overlay의 ModalOverlay가 담당한다（dialog.tsx 형제 포팅）.\n//\n// 포팅 판단（원본 대비 차이）:\n// - AlertDialogOverlayAlertDialogPortal export 제거 — ModalOverlay로 통합（dialog\n//   포팅과 동일）. role은 \"alertdialog\"（aria-modal은 ModalOverlay가 부여）.\n// - 원본 data-size 그룹 셀렉터（data-［size=sm］:max-w-xs, group-data-［size=］/\n//  alert-dialog-content:）는 data 속성이 없어 cva size 변형 + React context로\n//  전파한다（alert.tsx variant context 관행）. 헤더푸터가 콘텐츠의 size를 읽는다.\n// - 헤더의 gridgrid-rows-［auto_1fr］has-data-［slot=alert-dialog-media］ 2열 배치\n//  （sm 기본 크기에서 미디어 아이콘 좌측텍스트 우측）는 임의값그룹 셀렉터라 재현\n//  불가 — flex-col 근사: 미디어 아이콘은 항상 텍스트 위 중앙（모바일 배치）에 둔다\n//  （로드맵）. Title의 col-start-2Media의 row-span-2도 같은 이유로 소실.\n// - 푸터 sm 크기의 grid grid-cols-2（취소확인 2분할） 토큰은 유지하되, 네이티브\n//  Yoga에 grid가 없어 flex-col로 강등된다（웹만 2분할 — 한계）.\n// - AlertDialogActionCancel은 원본 Button asChild 래핑 대신 Button을 직접 렌더\n//  하고 variantsize 기본값（Action=default, Cancel=outline）과 닫기 연결\n//  （radix Action/Cancel은 눌림 시 다이얼로그를 닫는다）을 유지한다.\n// - 백드롭 눌림 닫기: radix AlertDialog는 외부 클릭으로 닫히지 않는다（경고 대화\n//  상자는 명시적 선택만 허용）. ModalOverlay dismissOnBackdrop={false}로 원본\n//  동작을 유지한다（골격에 옵션 반영 완료 — T-1a 제기）. ESC 닫기도 radix와\n//  같이 차단한다（dismissOnEscape={false} — G3 리뷰 제기）. 안드로이드 뒤로가기\n//  （onRequestClose）는 그대로 유지된다.\n// - 등장/퇴장 애니메이션（fadezoom）duration 토큰은 data-［state］ 의존이라 소실\n//  — 마운트/언마운트 즉시 전환（로드맵）. max-w-［calc（100%-2rem）］ → 래퍼 p-4 근사.\n// - 접근성 연결（dialog 포팅과 동일）: 트리거 aria-expandedaria-controls ↔ 카드\n//  nativeID（useId 토대）, 카드 aria-describedby ↔ 설명 nativeID, 오버레이\n//  aria-labelledby ↔ 제목 nativeID（스크린리더가 제목을 대화상자 이름으로 읽는다）.\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n  type RefObject,\n} from \"react\";\nimport {\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\";\nimport { cva } from \"class-variance-authority\";\n\nimport { Button, type ButtonProps } from \"./button\";\nimport { ModalOverlay } from \"../lib/overlay\";\nimport { cn } from \"@/lib/utils\";\n\ntype AlertDialogContextValue = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  /** 트리거 aria-controls ↔ 카드 nativeID 연결 */\n  contentId: string;\n  /** 제목 nativeID — 라벨 연결（aria-labelledby） 대기 토대 */\n  titleId: string;\n  /** 설명 nativeID — 카드 aria-describedby가 참조 */\n  descriptionId: string;\n  /** 트리거 요소 저장소 — ModalOverlay triggerRef（닫힘 후 포커스 복귀）로 전달 */\n  triggerRef: RefObject<unknown>;\n  setTriggerElement: (element: unknown) => void;\n};\n\nconst AlertDialogContext = createContext<AlertDialogContextValue | null>(null);\n\nfunction useAlertDialogContext(component: string): AlertDialogContextValue {\n  const ctx = useContext(AlertDialogContext);\n  if (!ctx) {\n    throw new Error(\n      `<${component}>는 <AlertDialog> 안에서 사용해야 합니다.`,\n    );\n  }\n  return ctx;\n}\n\n// 콘텐츠 size 변형 전파 — 원본 group-data-［size=］/alert-dialog-content 셀렉터\n// 대체. 콘텐츠 밖의 헤더푸터는 default 취급（alert.tsx 관행）.\nconst AlertDialogSizeContext = createContext<\"default\" | \"sm\">(\"default\");\n\n// 카드 — 원본 fixed 중앙 배치는 ModalOverlay position=\"center\"가 담당.\nconst alertDialogContentVariants = cva(\n  \"pointer-events-auto relative w-full gap-4 rounded-lg border bg-background p-6 shadow-lg unikit-dialog-in web:outline-none\",\n  {\n    variants: {\n      size: {\n        default: \"sm:max-w-lg\",\n        sm: \"max-w-xs\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  },\n);\n\n// 헤더 — 원본 grid place-items-center를 flex-col 근사. 기본 크기는 sm부터 좌측\n// 정렬（원본 sm:group-data-［size=default］ 토큰）, sm 크기는 중앙 유지.\nconst alertDialogHeaderVariants = cva(\n  \"flex-col items-center gap-1.5 text-center\",\n  {\n    variants: {\n      size: {\n        default: \"sm:items-start sm:text-left\",\n        sm: \"\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  },\n);\n\n// 푸터 — sm 크기는 취소확인 2분할（웹 grid, 네이티브 flex-col 강등）.\nconst alertDialogFooterVariants = cva(\"gap-2\", {\n  variants: {\n    size: {\n      default: \"flex-col-reverse sm:flex-row sm:justify-end\",\n      sm: \"grid grid-cols-2\",\n    },\n  },\n  defaultVariants: {\n    size: \"default\",\n  },\n});\n\nexport type AlertDialogProps = ViewProps & {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\nconst AlertDialog = forwardRef<BridgeViewRef, AlertDialogProps>(\n  ({ open, defaultOpen = false, onOpenChange, children, ...props }, ref) => {\n    // 비제어 상태 — 제어（open 지정）면 무시된다（tabsaccordion 관행）.\n    const [internalOpen, setInternalOpen] = useState(defaultOpen);\n    const isOpen = 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 baseId = useId();\n    const triggerRef = useRef<unknown>(null);\n    const setTriggerElement = useCallback((element: unknown) => {\n      triggerRef.current = element;\n    }, []);\n    const contextValue = useMemo<AlertDialogContextValue>(\n      () => ({\n        open: isOpen,\n        setOpen,\n        contentId: `${baseId}-alert-dialog-content`,\n        titleId: `${baseId}-alert-dialog-title`,\n        descriptionId: `${baseId}-alert-dialog-description`,\n        triggerRef,\n        setTriggerElement,\n      }),\n      [isOpen, setOpen, baseId, setTriggerElement],\n    );\n    return (\n      <AlertDialogContext.Provider value={contextValue}>\n        <View ref={ref} {...props}>\n          {children}\n        </View>\n      </AlertDialogContext.Provider>\n    );\n  },\n);\nAlertDialog.displayName = \"AlertDialog\";\n\nexport type AlertDialogTriggerProps = PressableProps & {\n  // asChild 미지원 — 문자열 children은 내부 Text로 감싸고, ReactNode는 그대로.\n  children?: ReactNode;\n};\n\nconst AlertDialogTrigger = forwardRef<\n  BridgePressableRef,\n  AlertDialogTriggerProps\n>(({ className, children, disabled, onPress, ...props }, forwardedRef) => {\n  const dialog = useAlertDialogContext(\"AlertDialogTrigger\");\n  // 콜백 ref — 외부 ref 병합 + 트리거 요소를 컨텍스트에 등록（포커스 복귀 대상）.\n  const setRef = (element: BridgePressableRef | null) => {\n    dialog.setTriggerElement(element);\n    if (typeof forwardedRef === \"function\") forwardedRef(element);\n    else if (forwardedRef) forwardedRef.current = element;\n  };\n  return (\n    <Pressable\n      ref={setRef}\n      accessibilityRole=\"button\"\n      aria-expanded={dialog.open}\n      aria-controls={dialog.contentId}\n      disabled={disabled}\n      onPress={() => {\n        dialog.setOpen(true);\n        onPress?.();\n      }}\n      className={className}\n      {...props}\n    >\n      {typeof children === \"string\" || typeof children === \"number\" ? (\n        <Text className=\"font-sans text-sm font-medium text-foreground\">\n          {children}\n        </Text>\n      ) : (\n        children\n      )}\n    </Pressable>\n  );\n});\nAlertDialogTrigger.displayName = \"AlertDialogTrigger\";\n\nexport type AlertDialogContentProps = ViewProps & {\n  size?: \"default\" | \"sm\";\n};\n\nconst AlertDialogContent = forwardRef<BridgeViewRef, AlertDialogContentProps>(\n  ({ className, children, size = \"default\", nativeID, ...props }, ref) => {\n    const dialog = useAlertDialogContext(\"AlertDialogContent\");\n    return (\n      <ModalOverlay\n        visible={dialog.open}\n        onClose={() => dialog.setOpen(false)}\n        position=\"center\"\n        // z-50는 오버레이 중첩 규칙 토큰（§2.3）, p-4는 원본 max-w-［calc（100%-2rem）］\n        // 의 표준 계단 근사（좌우 여백 1rem씩）.\n        className=\"z-50 p-4\"\n        backdropClassName=\"z-50\"\n        triggerRef={dialog.triggerRef}\n        accessibilityRole=\"alertdialog\"\n        aria-labelledby={dialog.titleId}\n        dismissOnBackdrop={false}\n        dismissOnEscape={false}\n      >\n        <AlertDialogSizeContext.Provider value={size}>\n          <View\n            ref={ref}\n            nativeID={nativeID ?? dialog.contentId}\n            aria-describedby={dialog.descriptionId}\n            className={cn(alertDialogContentVariants({ size }), className)}\n            {...props}\n          >\n            {children}\n          </View>\n        </AlertDialogSizeContext.Provider>\n      </ModalOverlay>\n    );\n  },\n);\nAlertDialogContent.displayName = \"AlertDialogContent\";\n\nexport type AlertDialogHeaderProps = ViewProps;\n\nconst AlertDialogHeader = forwardRef<BridgeViewRef, AlertDialogHeaderProps>(\n  ({ className, ...props }, ref) => {\n    const size = useContext(AlertDialogSizeContext);\n    return (\n      <View\n        ref={ref}\n        className={cn(alertDialogHeaderVariants({ size }), className)}\n        {...props}\n      />\n    );\n  },\n);\nAlertDialogHeader.displayName = \"AlertDialogHeader\";\n\nexport type AlertDialogFooterProps = ViewProps;\n\nconst AlertDialogFooter = forwardRef<BridgeViewRef, AlertDialogFooterProps>(\n  ({ className, ...props }, ref) => {\n    const size = useContext(AlertDialogSizeContext);\n    return (\n      <View\n        ref={ref}\n        className={cn(alertDialogFooterVariants({ size }), className)}\n        {...props}\n      />\n    );\n  },\n);\nAlertDialogFooter.displayName = \"AlertDialogFooter\";\n\nexport type AlertDialogTitleProps = TextProps;\n\nconst AlertDialogTitle = forwardRef<BridgeTextRef, AlertDialogTitleProps>(\n  ({ className, nativeID, ...props }, ref) => {\n    const dialog = useAlertDialogContext(\"AlertDialogTitle\");\n    return (\n      <Text\n        ref={ref}\n        nativeID={nativeID ?? dialog.titleId}\n        // 이중 지정: 전경색은 Text에（네이티브는 색 상속 불가）. 원본의\n        // col-start-2（미디어 2열 배치）는 grid 소실로 제거.\n        className={cn(\n          \"font-sans text-lg font-semibold text-foreground\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nAlertDialogTitle.displayName = \"AlertDialogTitle\";\n\nexport type AlertDialogDescriptionProps = TextProps;\n\nconst AlertDialogDescription = forwardRef<\n  BridgeTextRef,\n  AlertDialogDescriptionProps\n>(({ className, nativeID, ...props }, ref) => {\n  const dialog = useAlertDialogContext(\"AlertDialogDescription\");\n  return (\n    <Text\n      ref={ref}\n      nativeID={nativeID ?? dialog.descriptionId}\n      className={cn(\"font-sans text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n});\nAlertDialogDescription.displayName = \"AlertDialogDescription\";\n\nexport type AlertDialogMediaProps = ViewProps;\n\nconst AlertDialogMedia = forwardRef<BridgeViewRef, AlertDialogMediaProps>(\n  ({ className, ...props }, ref) => (\n    // self-center — 원본 inline-flex의 콘텐츠 폭 수축을 교차축 정렬로 근사.\n    // 원본 ［*:［svg］］:size-8 아이콘 규격은 글리프/아이콘 사용처 className으로.\n    <View\n      ref={ref}\n      className={cn(\n        \"mb-2 flex size-16 items-center justify-center self-center rounded-md bg-muted\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nAlertDialogMedia.displayName = \"AlertDialogMedia\";\n\n// Action/Cancel — 원본 Button asChild 래핑을 직렬화하고 닫기 연결만 이식.\nexport type AlertDialogActionProps = ButtonProps;\n\nconst AlertDialogAction = forwardRef<\n  BridgePressableRef,\n  AlertDialogActionProps\n>(\n  (\n    { className, variant = \"default\", size = \"default\", onPress, ...props },\n    ref,\n  ) => {\n    const dialog = useAlertDialogContext(\"AlertDialogAction\");\n    return (\n      <Button\n        ref={ref}\n        variant={variant}\n        size={size}\n        className={className}\n        onPress={() => {\n          dialog.setOpen(false);\n          onPress?.();\n        }}\n        {...props}\n      />\n    );\n  },\n);\nAlertDialogAction.displayName = \"AlertDialogAction\";\n\nexport type AlertDialogCancelProps = ButtonProps;\n\nconst AlertDialogCancel = forwardRef<\n  BridgePressableRef,\n  AlertDialogCancelProps\n>(\n  (\n    { className, variant = \"outline\", size = \"default\", onPress, ...props },\n    ref,\n  ) => {\n    const dialog = useAlertDialogContext(\"AlertDialogCancel\");\n    return (\n      <Button\n        ref={ref}\n        variant={variant}\n        size={size}\n        className={className}\n        onPress={() => {\n          dialog.setOpen(false);\n          onPress?.();\n        }}\n        {...props}\n      />\n    );\n  },\n);\nAlertDialogCancel.displayName = \"AlertDialogCancel\";\n\nexport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogMedia,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n  alertDialogContentVariants,\n  alertDialogHeaderVariants,\n  alertDialogFooterVariants,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/alert-dialog.tsx"
    }
  ],
  "type": "registry:ui"
}
