{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sheet",
  "title": "Sheet",
  "description": "shadcn/ui(new-york-v4) sheet의 유니버설(웹+네이티브 한 벌 소스) 포팅. 상태는 React state + context로 직접 관리한다. 포털·백드롭·포커스 관리·ESC 닫기·스크롤 잠금은 lib/overlay의 ModalOverlay가 담당한다(dialog.tsx 형제).",
  "dependencies": [
    "@unikit-bridge/ui",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils",
    "animations"
  ],
  "files": [
    {
      "path": "registry/ui/sheet.tsx",
      "content": "// Sheet — shadcn/ui（new-york-v4） sheet.tsx 유니버설 포팅 （사이클 40, T-1a）.\n// 원본: shadcn-ref/ui/sheet.tsx — radix Dialog（시트 변형） 엔진을 제거하고 열림\n// 상태는 React state + context로 직접 관리한다. 포털백드롭포커스 관리ESC\n// 닫기스크롤 잠금은 lib/overlay의 ModalOverlay가 담당한다（dialog.tsx 형제）.\n//\n// 포팅 판단（원본 대비 차이）:\n// - 기본 side를 원본 \"right\" → \"bottom\"으로 변경（사이클 40 브리프 — 모바일\n//   바텀시트 우선, sm에서도 bottom 유지. 원본 4방향 전부 side prop으로 지원）.\n// - 원본의 fixed + inset-*-0 가장자리 고정 배치는 ModalOverlay position\n//   （top/right/bottom/left — 래퍼 flex 정렬）이 담당하고, 카드는 남는 축 토큰\n//   （높이 h-full폭 w-3/4 sm:max-w-sm테두리 border-l/r/t/b）만 유지한다.\n// - 등장/퇴장 애니메이션（slide-in/outtransitionease-in-outduration）은\n//   data-［state］ 의존이라 소실 — 마운트/언마운트 즉시 전환（로드맵）.\n// - XIcon — 브리지 lucide 아이콘 프리미티브（primitives 경유）. sr-only \"Close\"는\n//   accessibilityLabel로 대체（dialog 포팅과 동일）.\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// - 원본 SheetOverlaySheetPortal은 export 목록에 없지만（내부 사용） 포팅은\n//   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  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\";\nimport { cva } from \"class-variance-authority\";\n\nimport { ModalOverlay, type ModalOverlayPosition } from \"../lib/overlay\";\nimport { cn } from \"@/lib/utils\";\n\ntype SheetContextValue = {\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 SheetContext = createContext<SheetContextValue | null>(null);\n\nfunction useSheetContext(component: string): SheetContextValue {\n  const ctx = useContext(SheetContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <Sheet> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\n// 카드 변형 — 남는 축 토큰만 원본에서 유지（가장자리 고정은 ModalOverlay가 담당）.\nconst sheetContentVariants = cva(\n  \"pointer-events-auto relative flex-col gap-4 bg-background shadow-lg\",\n  {\n    variants: {\n      side: {\n        top: \"w-full border-b unikit-sheet-in-top\",\n        bottom: \"w-full 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      side: \"bottom\",\n    },\n  },\n);\n\nexport type SheetProps = ViewProps & {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\nconst Sheet = forwardRef<BridgeViewRef, SheetProps>(\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<SheetContextValue>(\n      () => ({\n        open: isOpen,\n        setOpen,\n        contentId: `${baseId}-sheet-content`,\n        titleId: `${baseId}-sheet-title`,\n        descriptionId: `${baseId}-sheet-description`,\n        triggerRef,\n        setTriggerElement,\n      }),\n      [isOpen, setOpen, baseId, setTriggerElement],\n    );\n    return (\n      <SheetContext.Provider value={contextValue}>\n        <View ref={ref} {...props}>\n          {children}\n        </View>\n      </SheetContext.Provider>\n    );\n  },\n);\nSheet.displayName = \"Sheet\";\n\nexport type SheetTriggerProps = PressableProps & {\n  // asChild 미지원 — 문자열 children은 내부 Text로 감싸고, ReactNode는 그대로.\n  children?: ReactNode;\n};\n\nconst SheetTrigger = forwardRef<BridgePressableRef, SheetTriggerProps>(\n  ({ className, children, disabled, onPress, ...props }, forwardedRef) => {\n    const sheet = useSheetContext(\"SheetTrigger\");\n    // 콜백 ref — 외부 ref 병합 + 트리거 요소를 컨텍스트에 등록（포커스 복귀 대상）.\n    const setRef = (element: BridgePressableRef | null) => {\n      sheet.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={sheet.open}\n        aria-controls={sheet.contentId}\n        disabled={disabled}\n        onPress={() => {\n          sheet.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);\nSheetTrigger.displayName = \"SheetTrigger\";\n\nexport type SheetCloseProps = PressableProps & {\n  // SheetTrigger와 동일 — 문자열 children은 내부 Text로 감싼다.\n  children?: ReactNode;\n};\n\nconst SheetClose = forwardRef<BridgePressableRef, SheetCloseProps>(\n  ({ className, children, disabled, onPress, ...props }, ref) => {\n    const sheet = useSheetContext(\"SheetClose\");\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"button\"\n        disabled={disabled}\n        onPress={() => {\n          sheet.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);\nSheetClose.displayName = \"SheetClose\";\n\n// 기본 side는 원본 shadcn 기본값 \"right\"를 유지한다（사이클 40 초안이\n// \"bottom\"으로 두었던 것을 G3 F-12 지적으로 복원 — API 기본값 호환）.\nexport type SheetContentProps = ViewProps & {\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  showCloseButton?: boolean;\n};\n\nconst SheetContent = forwardRef<BridgeViewRef, SheetContentProps>(\n  (\n    { className, children, side = \"right\", showCloseButton = true, nativeID, ...props },\n    ref,\n  ) => {\n    const sheet = useSheetContext(\"SheetContent\");\n    return (\n      <ModalOverlay\n        visible={sheet.open}\n        onClose={() => sheet.setOpen(false)}\n        position={side as ModalOverlayPosition}\n        // z-50는 오버레이 중첩 규칙 토큰（§2.3）. 시트는 가장자리에 붙으므로\n        // 래퍼 여백 없음（원본 inset-0 flush 배치와 동일）.\n        className=\"z-50\"\n        backdropClassName=\"z-50\"\n        triggerRef={sheet.triggerRef}\n        accessibilityRole=\"dialog\"\n        aria-labelledby={sheet.titleId}\n      >\n        <View\n          ref={ref}\n          nativeID={nativeID ?? sheet.contentId}\n          aria-describedby={sheet.descriptionId}\n          className={cn(sheetContentVariants({ side }), className)}\n          {...props}\n        >\n          {children}\n          {showCloseButton && (\n            <SheetClose\n              accessibilityLabel=\"Close\"\n              // 원본 data-[state=open]:bg-secondary — 닫기 버튼은 열린 상태에서만\n              // 보이므로 상시 적용과 동일. rounded-xs·무패딩 원본값.\n              className={cn(\n                \"absolute top-4 right-4 rounded-xs bg-secondary 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              {/* 원본은 전경 클래스가 없어（부모 상속 foreground） — 이중 지정\n                  원칙으로 내부 아이콘에 text-foreground를 명시한다. */}\n              <XIcon className=\"size-4 text-foreground\" />\n            </SheetClose>\n          )}\n        </View>\n      </ModalOverlay>\n    );\n  },\n);\nSheetContent.displayName = \"SheetContent\";\n\nexport type SheetHeaderProps = ViewProps;\n\nconst SheetHeader = forwardRef<BridgeViewRef, SheetHeaderProps>(\n  ({ className, ...props }, ref) => (\n    <View ref={ref} className={cn(\"gap-1.5 p-4\", className)} {...props} />\n  ),\n);\nSheetHeader.displayName = \"SheetHeader\";\n\nexport type SheetFooterProps = ViewProps;\n\nconst SheetFooter = forwardRef<BridgeViewRef, SheetFooterProps>(\n  ({ className, ...props }, ref) => (\n    <View ref={ref} className={cn(\"mt-auto gap-2 p-4\", className)} {...props} />\n  ),\n);\nSheetFooter.displayName = \"SheetFooter\";\n\nexport type SheetTitleProps = TextProps;\n\nconst SheetTitle = forwardRef<BridgeTextRef, SheetTitleProps>(\n  ({ className, nativeID, ...props }, ref) => {\n    const sheet = useSheetContext(\"SheetTitle\");\n    return (\n      <Text\n        ref={ref}\n        nativeID={nativeID ?? sheet.titleId}\n        className={cn(\n          \"font-sans font-semibold text-foreground\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nSheetTitle.displayName = \"SheetTitle\";\n\nexport type SheetDescriptionProps = TextProps;\n\nconst SheetDescription = forwardRef<BridgeTextRef, SheetDescriptionProps>(\n  ({ className, nativeID, ...props }, ref) => {\n    const sheet = useSheetContext(\"SheetDescription\");\n    return (\n      <Text\n        ref={ref}\n        nativeID={nativeID ?? sheet.descriptionId}\n        className={cn(\"font-sans text-sm text-muted-foreground\", className)}\n        {...props}\n      />\n    );\n  },\n);\nSheetDescription.displayName = \"SheetDescription\";\n\nexport {\n  Sheet,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n  sheetContentVariants,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/sheet.tsx"
    }
  ],
  "type": "registry:ui"
}
