{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dialog",
  "title": "Dialog",
  "description": "shadcn/ui(new-york-v4) dialog의 유니버설(웹+네이티브 한 벌 소스) 포팅. React state + context로 직접 관리한다. 포털·백드롭(딤)·포커스 이동/트랩/복귀· ESC 닫기·배경 스크롤 잠금은 lib/overlay의 ModalOverlay(브리지 오버레이 훈)가",
  "dependencies": [
    "@unikit-bridge/ui"
  ],
  "registryDependencies": [
    "utils",
    "animations"
  ],
  "files": [
    {
      "path": "registry/ui/dialog.tsx",
      "content": "// Dialog — shadcn/ui（new-york-v4） dialog.tsx 유니버설 포팅 （사이클 40, T-1a）.\n// 원본: shadcn-ref/ui/dialog.tsx — radix Dialog 엔진을 제거하고 열림 상태는\n// React state + context로 직접 관리한다. 포털백드롭（딤）포커스 이동/트랩/복귀\n// ESC 닫기배경 스크롤 잠금은 lib/overlay의 ModalOverlay（브리지 오버레이 훈）가\n// 담당한다（재구현 금지 — 디자인 계획 §2.3 \"dialog 계열은 포털+백드롭만 소비\"）.\n//\n// 포팅 판단（원본 대비 차이）:\n// - DialogOverlayDialogPortal export 제거 — 포털과 오버레이（백드롭）가 ModalOverlay\n//   하나로 통합되었다. z-50은 오버레이 중첩 규칙 토큰으로 래퍼백드롭에 지정한다.\n// - 원본 콘텐츠의 fixed + top/left 50% + translate 중앙 배치는 ModalOverlay\n//   position=\"center\"（flex 중앙 정렬）로 대체. 임의값 max-w-［calc（100%-2rem）］ 금지에\n//   따라 래퍼에 p-4（좌우 1rem씩 = 100%-2rem 상당）를 줘 카드가 화면 가장자리에\n//   닿지 않게 한다. grid gap-4 → 브리지 View 기본 flex-col + gap-4로 동등 재현.\n// - data-［state］ 기반 등장/퇴장 애니메이션（fadezoomduration）은 data 속성이 없어\n//   재현 불가 — 마운트/언마운트 즉시 전환（로드맵: 웹 전용 애니메이션 클래스）.\n// - XIcon — 브리지 lucide 아이콘 프리미티브（primitives 경유）. sr-only \"Close\"는\n//   accessibilityLabel로 대체（유니버설 sr-only 유틸 부재 — 디자인 계획 §3）.\n// - 트리거닫기는 radix asChild 대신 Pressable 직렬화 — 문자열 children은 내부\n//   Text로 감싸고（pagination 관행） ReactNode는 그대로 둔다（사용처가 직접 스타일링）.\n// - 접근성 연결: role=\"dialog\"aria-modal은 ModalOverlay가 부여하고, 트리거\n//   aria-expandedaria-controls와 카드 nativeID를 useId 토대로 결정적으로 연결한다.\n//   카드에 aria-describedby（설명 nativeID）, 오버레이에 aria-labelledby（제목\n//   nativeID）를 건다 — 스크린리더가 제목을 대화상자 이름으로 읽는다（골격에\n//   계약 반영 완료, T-1a 제기）.\n// - 닫힘 후 트리거 포커스 복귀: 트리거가 컨텍스트에 요소를 등록하고 ModalOverlay\n//   triggerRef로 전달한다（useOverlayFocus가 복귀 처리）.\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  XIcon,\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 { Button } from \"./button\";\nimport { ModalOverlay } from \"../lib/overlay\";\nimport { cn } from \"@/lib/utils\";\n\ntype DialogContextValue = {\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 DialogContext = createContext<DialogContextValue | null>(null);\n\nfunction useDialogContext(component: string): DialogContextValue {\n  const ctx = useContext(DialogContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <Dialog> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\nexport type DialogProps = ViewProps & {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\nconst Dialog = forwardRef<BridgeViewRef, DialogProps>(\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<DialogContextValue>(\n      () => ({\n        open: isOpen,\n        setOpen,\n        contentId: `${baseId}-dialog-content`,\n        titleId: `${baseId}-dialog-title`,\n        descriptionId: `${baseId}-dialog-description`,\n        triggerRef,\n        setTriggerElement,\n      }),\n      [isOpen, setOpen, baseId, setTriggerElement],\n    );\n    return (\n      <DialogContext.Provider value={contextValue}>\n        <View ref={ref} {...props}>\n          {children}\n        </View>\n      </DialogContext.Provider>\n    );\n  },\n);\nDialog.displayName = \"Dialog\";\n\nexport type DialogTriggerProps = PressableProps & {\n  // asChild 미지원 — 문자열 children은 내부 Text로 감싸고, ReactNode는 그대로.\n  children?: ReactNode;\n};\n\nconst DialogTrigger = forwardRef<BridgePressableRef, DialogTriggerProps>(\n  ({ className, children, disabled, onPress, ...props }, forwardedRef) => {\n    const dialog = useDialogContext(\"DialogTrigger\");\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  },\n);\nDialogTrigger.displayName = \"DialogTrigger\";\n\nexport type DialogCloseProps = PressableProps & {\n  // DialogTrigger와 동일 — 문자열 children은 내부 Text로 감싼다.\n  children?: ReactNode;\n};\n\nconst DialogClose = forwardRef<BridgePressableRef, DialogCloseProps>(\n  ({ className, children, disabled, onPress, ...props }, ref) => {\n    const dialog = useDialogContext(\"DialogClose\");\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"button\"\n        disabled={disabled}\n        onPress={() => {\n          dialog.setOpen(false);\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  },\n);\nDialogClose.displayName = \"DialogClose\";\n\nexport type DialogContentProps = ViewProps & {\n  showCloseButton?: boolean;\n};\n\nconst DialogContent = forwardRef<BridgeViewRef, DialogContentProps>(\n  (\n    { className, children, showCloseButton = true, nativeID, ...props },\n    ref,\n  ) => {\n    const dialog = useDialogContext(\"DialogContent\");\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=\"dialog\"\n        aria-labelledby={dialog.titleId}\n      >\n        <View\n          ref={ref}\n          nativeID={nativeID ?? dialog.contentId}\n          aria-describedby={dialog.descriptionId}\n          className={cn(\n            // pointer-events-auto — 래퍼 box-none 상속 회피（lib/overlay 머리주석）.\n            \"pointer-events-auto relative w-full gap-4 rounded-lg border bg-background p-6 shadow-lg sm:max-w-lg unikit-dialog-in web:outline-none\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n          {showCloseButton && (\n            <DialogClose\n              accessibilityLabel=\"Close\"\n              // 원본 data-[state=open]:bg-accent·text-muted-foreground — 닫기 버튼은\n              // 열린 상태에서만 보이므로 상시 적용과 동일. rounded-xs·무패딩 원본값.\n              className={cn(\n                \"absolute top-4 right-4 rounded-xs bg-accent opacity-70\",\n                \"web:transition-opacity web:hover:opacity-100\",\n                \"web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2 web:focus-visible:ring-offset-background\",\n              )}\n            >\n              <XIcon className=\"size-4 text-muted-foreground\" />\n            </DialogClose>\n          )}\n        </View>\n      </ModalOverlay>\n    );\n  },\n);\nDialogContent.displayName = \"DialogContent\";\n\nexport type DialogHeaderProps = ViewProps;\n\nconst DialogHeader = forwardRef<BridgeViewRef, DialogHeaderProps>(\n  ({ className, ...props }, ref) => (\n    <View\n      ref={ref}\n      className={cn(\"gap-2 text-center sm:text-left\", className)}\n      {...props}\n    />\n  ),\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nexport type DialogFooterProps = ViewProps & {\n  showCloseButton?: boolean;\n};\n\nconst DialogFooter = forwardRef<BridgeViewRef, DialogFooterProps>(\n  ({ className, showCloseButton = false, children, ...props }, ref) => {\n    // 문맥 밖 단독 사용 시 자동 닫기 버튼은 생략한다（레이아웃 역할 유지）.\n    const dialog = useContext(DialogContext);\n    return (\n      <View\n        ref={ref}\n        className={cn(\n          \"gap-2 flex-col-reverse sm:flex-row sm:justify-end\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && dialog && (\n          <Button variant=\"outline\" onPress={() => dialog.setOpen(false)}>\n            Close\n          </Button>\n        )}\n      </View>\n    );\n  },\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nexport type DialogTitleProps = TextProps;\n\nconst DialogTitle = forwardRef<BridgeTextRef, DialogTitleProps>(\n  ({ className, nativeID, ...props }, ref) => {\n    const dialog = useDialogContext(\"DialogTitle\");\n    return (\n      <Text\n        ref={ref}\n        nativeID={nativeID ?? dialog.titleId}\n        className={cn(\n          \"font-sans text-lg leading-none font-semibold text-foreground\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nDialogTitle.displayName = \"DialogTitle\";\n\nexport type DialogDescriptionProps = TextProps;\n\nconst DialogDescription = forwardRef<BridgeTextRef, DialogDescriptionProps>(\n  ({ className, nativeID, ...props }, ref) => {\n    const dialog = useDialogContext(\"DialogDescription\");\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  },\n);\nDialogDescription.displayName = \"DialogDescription\";\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/dialog.tsx"
    }
  ],
  "type": "registry:ui"
}
