{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drawer",
  "title": "Drawer",
  "description": "shadcn/ui(new-york-v4) drawer의 유니버설(웹+네이티브 한 벌 소스) 포팅. 상태는 React state + context로 직접 관리한다. 포털·백드롭·포커스 관리·ESC 닫기·스크롤 잠금은 lib/overlay의 ModalOverlay가, 드래그는 브리지 useDrag가",
  "dependencies": [
    "@unikit-bridge/ui",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils",
    "animations"
  ],
  "files": [
    {
      "path": "registry/ui/drawer.tsx",
      "content": "// Drawer — shadcn/ui（new-york-v4） drawer.tsx 유니버설 포팅 （사이클 40, T-1a）.\n// 원본: shadcn-ref/ui/drawer.tsx — vaul（드래그 바텀시트 엔진）을 제거하고 열림\n// 상태는 React state + context로 직접 관리한다. 포털백드롭포커스 관리ESC\n// 닫기스크롤 잠금은 lib/overlay의 ModalOverlay가, 드래그는 브리지 useDrag가\n// 담당한다（dialog.tsxsheet.tsx 형제 포팅）.\n//\n// 포팅 판단（원본 대비 차이）:\n// - vaul 제거 — 드래그로 닫기（디자인 계획 §3）만 useDrag로 최소 재현: 바텀\n//   방향 핸들에서 아래로 80px 이상 끌면 닫는다. 핸들에서만 드래그를 받아 카드\n//   안 버튼스크롤과 충돌하지 않게 했다. vaul의 나머지 기능（콘텐츠 스크롤과\n//   제스처 통합스냅 루버 밴드드래그 중 카드 추적 이동키패드 대응）은 미지원\n//   — 로드맵. topleftright 방향 드래그 닫기도 미지원.\n// - direction은 원본 vaul 기본값 \"bottom\" 유지, 브리프 지정 방향 leftright를\n//   포함해 4방향 전부 지원한다（시트와 구분: drawer는 핸들+드래그 닫기+높이 제한）.\n// - 원본 data-［vaul-drawer-direction=］ 그룹 셀렉터 → cva direction 변형 +\n//   React context 전파（헤더 정렬용 — alert.tsx 관행）. 핸들 표시 여부도 원본\n//   group-data 셀렉터 대신 direction === \"bottom\" 조건부 렌더로 전개.\n// - 임의값 클래스 금지에 따라 w-［100px］ → w-24, max-h-［80vh］ → max-h-96（24rem\n//   고정 근사 — 뷰포트 비례 높이는 로드맵: BridgeStyle 퍼센트 또는 사용처\n//   className 오버라이드）. mt-24/mb-24（드래그 여유 공간）rounded-t/b-lg\n//   sm:max-w-sm 토큰은 유지.\n// - 등장/퇴장 애니메이션（fade）은 data-［state］ 의존이라 소실 — 마운트/언마운트\n//   즉시 전환（로드맵）. 원본은 닫기 버튼이 없어（드래그백드롭 닫기） 포팅도\n//   동일하다（웹 ESC백드롭 눌림은 ModalOverlay가 담당）.\n// - 트리거닫기는 asChild 대신 Pressable 직렬화（문자열 children → 내부 Text）.\n// - 접근성 연결（dialog 포팅과 동일）: role=\"dialog\"aria-modal은 ModalOverlay,\n//   트리거 aria-expandedaria-controls ↔ 카드 nativeID（useId 토대）, 카드\n//   aria-describedby ↔ 설명 nativeID, aria-labelledby ↔ 제목 nativeID\n//   （ModalOverlay prop으로 전달 — G3 리뷰 제기 반영）.\n// - DrawerOverlayDrawerPortal export 제거 — ModalOverlay로 통합.\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  useDrag,\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 { ModalOverlay, type ModalOverlayPosition } from \"../lib/overlay\";\nimport { cn } from \"@/lib/utils\";\n\ntype DrawerContextValue = {\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 DrawerContext = createContext<DrawerContextValue | null>(null);\n\nfunction useDrawerContext(component: string): DrawerContextValue {\n  const ctx = useContext(DrawerContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <Drawer> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\n// 방향 context — 원본 group-data-［vaul-drawer-direction］ 셀렉터 대체（헤더 정렬）.\nconst DrawerDirectionContext = createContext<\"top\" | \"right\" | \"bottom\" | \"left\">(\n  \"bottom\",\n);\n\n// 카드 변형 — 남는 축 토큰 유지（가장자리 고정은 ModalOverlay position이 담당）.\n// bottom/top의 mt-24/mb-24는 드래그 여유 공간（vaul 관행）이자 높이 상한의 역할.\nconst drawerContentVariants = cva(\n  \"pointer-events-auto relative flex-col bg-background\",\n  {\n    variants: {\n      direction: {\n        top: \"w-full mb-24 max-h-96 rounded-b-lg border-b unikit-sheet-in-top\",\n        bottom: \"w-full mt-24 max-h-96 rounded-t-lg border-t unikit-sheet-in-bottom\",\n        left: \"h-full w-3/4 border-r unikit-sheet-in-left sm:max-w-sm\",\n        right: \"h-full w-3/4 border-l unikit-sheet-in-right sm:max-w-sm\",\n      },\n    },\n    defaultVariants: {\n      direction: \"bottom\",\n    },\n  },\n);\n\nexport type DrawerProps = ViewProps & {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\nconst Drawer = forwardRef<BridgeViewRef, DrawerProps>(\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<DrawerContextValue>(\n      () => ({\n        open: isOpen,\n        setOpen,\n        contentId: `${baseId}-drawer-content`,\n        titleId: `${baseId}-drawer-title`,\n        descriptionId: `${baseId}-drawer-description`,\n        triggerRef,\n        setTriggerElement,\n      }),\n      [isOpen, setOpen, baseId, setTriggerElement],\n    );\n    return (\n      <DrawerContext.Provider value={contextValue}>\n        <View ref={ref} {...props}>\n          {children}\n        </View>\n      </DrawerContext.Provider>\n    );\n  },\n);\nDrawer.displayName = \"Drawer\";\n\nexport type DrawerTriggerProps = PressableProps & {\n  // asChild 미지원 — 문자열 children은 내부 Text로 감싸고, ReactNode는 그대로.\n  children?: ReactNode;\n};\n\nconst DrawerTrigger = forwardRef<BridgePressableRef, DrawerTriggerProps>(\n  ({ className, children, disabled, onPress, ...props }, forwardedRef) => {\n    const drawer = useDrawerContext(\"DrawerTrigger\");\n    // 콜백 ref — 외부 ref 병합 + 트리거 요소를 컨텍스트에 등록（포커스 복귀 대상）.\n    const setRef = (element: BridgePressableRef | null) => {\n      drawer.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={drawer.open}\n        aria-controls={drawer.contentId}\n        disabled={disabled}\n        onPress={() => {\n          drawer.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);\nDrawerTrigger.displayName = \"DrawerTrigger\";\n\nexport type DrawerCloseProps = PressableProps & {\n  // DrawerTrigger와 동일 — 문자열 children은 내부 Text로 감싼다.\n  children?: ReactNode;\n};\n\nconst DrawerClose = forwardRef<BridgePressableRef, DrawerCloseProps>(\n  ({ className, children, disabled, onPress, ...props }, ref) => {\n    const drawer = useDrawerContext(\"DrawerClose\");\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"button\"\n        disabled={disabled}\n        onPress={() => {\n          drawer.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);\nDrawerClose.displayName = \"DrawerClose\";\n\nexport type DrawerContentProps = ViewProps & {\n  direction?: \"top\" | \"right\" | \"bottom\" | \"left\";\n};\n\nconst DrawerContent = forwardRef<BridgeViewRef, DrawerContentProps>(\n  ({ className, children, direction = \"bottom\", nativeID, ...props }, ref) => {\n    const drawer = useDrawerContext(\"DrawerContent\");\n    // 아래 드래그 닫기 — vaul 제스처의 최소 대체（핸들 전용, 임계값 80px）.\n    const { dragHandlers } = useDrag({\n      onMove: (event) => {\n        if (event.dy > 80) drawer.setOpen(false);\n      },\n    });\n    return (\n      <ModalOverlay\n        visible={drawer.open}\n        onClose={() => drawer.setOpen(false)}\n        position={direction as ModalOverlayPosition}\n        // z-50는 오버레이 중첩 규칙 토큰（§2.3）. 드로어는 가장자리에 붙는다.\n        className=\"z-50\"\n        backdropClassName=\"z-50\"\n        triggerRef={drawer.triggerRef}\n        accessibilityRole=\"dialog\"\n        aria-labelledby={drawer.titleId}\n      >\n        <DrawerDirectionContext.Provider value={direction}>\n          <View\n            ref={ref}\n            nativeID={nativeID ?? drawer.contentId}\n            aria-describedby={drawer.descriptionId}\n            className={cn(drawerContentVariants({ direction }), className)}\n            {...props}\n          >\n            {direction === \"bottom\" && (\n              <View\n                aria-hidden\n                // 드래그 핸들 — 웹 포인터 이벤트/네이티브 panHandlers 펼침.\n                {...(dragHandlers as ViewProps)}\n                className=\"mx-auto mt-4 h-2 w-24 shrink-0 rounded-full bg-muted\"\n              />\n            )}\n            {children}\n          </View>\n        </DrawerDirectionContext.Provider>\n      </ModalOverlay>\n    );\n  },\n);\nDrawerContent.displayName = \"DrawerContent\";\n\nexport type DrawerHeaderProps = ViewProps;\n\nconst DrawerHeader = forwardRef<BridgeViewRef, DrawerHeaderProps>(\n  ({ className, ...props }, ref) => {\n    const direction = useContext(DrawerDirectionContext);\n    return (\n      <View\n        ref={ref}\n        className={cn(\n          // 바텀탭 드로어는 중앙 정렬（원본 group-data 셀렉터）, md부터는 원본대로\n          // 좌측 정렬로 전환（md: 토큰이 나중에 이긴다 — 원본 캐스케이드 동일）.\n          \"gap-0.5 p-4\",\n          (direction === \"bottom\" || direction === \"top\") && \"text-center\",\n          \"md:gap-1.5 md:text-left\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nDrawerHeader.displayName = \"DrawerHeader\";\n\nexport type DrawerFooterProps = ViewProps;\n\nconst DrawerFooter = forwardRef<BridgeViewRef, DrawerFooterProps>(\n  ({ className, ...props }, ref) => (\n    <View ref={ref} className={cn(\"mt-auto gap-2 p-4\", className)} {...props} />\n  ),\n);\nDrawerFooter.displayName = \"DrawerFooter\";\n\nexport type DrawerTitleProps = TextProps;\n\nconst DrawerTitle = forwardRef<BridgeTextRef, DrawerTitleProps>(\n  ({ className, nativeID, ...props }, ref) => {\n    const drawer = useDrawerContext(\"DrawerTitle\");\n    return (\n      <Text\n        ref={ref}\n        nativeID={nativeID ?? drawer.titleId}\n        className={cn(\"font-sans font-semibold text-foreground\", className)}\n        {...props}\n      />\n    );\n  },\n);\nDrawerTitle.displayName = \"DrawerTitle\";\n\nexport type DrawerDescriptionProps = TextProps;\n\nconst DrawerDescription = forwardRef<BridgeTextRef, DrawerDescriptionProps>(\n  ({ className, nativeID, ...props }, ref) => {\n    const drawer = useDrawerContext(\"DrawerDescription\");\n    return (\n      <Text\n        ref={ref}\n        nativeID={nativeID ?? drawer.descriptionId}\n        className={cn(\"font-sans text-sm text-muted-foreground\", className)}\n        {...props}\n      />\n    );\n  },\n);\nDrawerDescription.displayName = \"DrawerDescription\";\n\nexport {\n  Drawer,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n  drawerContentVariants,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/drawer.tsx"
    }
  ],
  "type": "registry:ui"
}
