{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "popover",
  "title": "Popover",
  "description": "shadcn/ui(new-york-v4) popover의 유니버설(웹+네이티브 한 벌 소스) 포팅. React state + context로, 오버레이는 lib/overlay의 AnchoredOverlay로 구성한다. 포팅 판단(원본 대비 차이):",
  "dependencies": [
    "@unikit-bridge/ui",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils",
    "animations"
  ],
  "files": [
    {
      "path": "registry/ui/popover.tsx",
      "content": "// Popover — shadcn/ui（new-york-v4） popover.tsx 유니버설 포팅 （사이클 40, T-1b）.\n// 원본: shadcn-ref/ui/popover.tsx — radix Popover 엔진을 제거하고 열림 상태는\n// React state + context로, 오버레이는 lib/overlay의 AnchoredOverlay로 구성한다.\n//\n// 포팅 판단（원본 대비 차이）:\n// - radix RootTriggerContentAnchorPortal을 각각 Popover（상태 context）\n//   PopoverTrigger（Pressable 토글）PopoverContent（AnchoredOverlay + 카드 View）\n//   PopoverAnchor（앵커 오버라이드 View）로 재구성. Portal은 AnchoredOverlay가\n//   내부적으로 소비하므로 별도 파트가 없다（7종 export는 원본 그대로 유지）.\n// - 충돌 감지（collision paddingavoidCollisionssticky）는 미지원 —\n//   AnchoredOverlay의 뷰포트 클램프（여백 8px）로 근사된다. 원본과 달리 열리는\n//   방향（side）을 역방향으로 자동 전환하지는 않는다.\n// - 원본 Content의 sidesideOffset 프로퍼티명을 유지하되 내부적으로 AnchoredOverlay\n//   의 placementoffset로 매핑한다（기본 side \"bottom\"sideOffset 4align \"center\"\n//   은 원본 기본값 그대로）.\n// - 애니메이션（animate-in/outfadezoomdata-［side］ 슬라이드）과 origin 변형\n//   （radix CSS 변수）은 즉시 전환으로 근사 — 토큰 전부 제거.\n// - modal 폴백（radix Root modal）은 미지원（항상 비모달）. 바깥 포인터\n//   눌림 닫기는 AnchoredOverlay의 투명 백드롭이 잡아준다（onBackdropPress\n//   연결 — G3 리뷰 주석 정합 갱신）. ESC（브리지 useDismiss, 웹）와 트리거\n//   재토글도 닫는다.\n// - asChild 미지원 — 트리거는 항상 브리지 Pressable（웹 <button>）로 렌더된다.\n// - 카드 className에 pointer-events-auto 기본 포함（웹 box-none 상속 회피）.\n// - 접근성: 트리거 aria-expandedaria-controls（useId 기반 nativeID）, 카드\n//   role=\"dialog\"（원본 radix Content와 동일）. RN은 dialog rolearia-describedby\n//   를 무시한다（브리지 계약 문서화된 한계 — 웹 스크린리더만 이득）.\n// - text-popover-foreground 카드 토큰은 웹 상속용으로 유지하되, 네이티브는 색\n//   상속이 없어 Title/Description 내부 Text에 이중 지정한다（코드베이스 관행）.\n// - dark: 변형 토큰은 프로젝트 관행（미사용）대로 제거.\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ForwardedRef,\n  type MutableRefObject,\n} from \"react\";\nimport {\n  Pressable,\n  Text,\n  View,\n  type BridgePressableRef,\n  type BridgeViewRef,\n  type PressableProps,\n  type ViewProps,\n} from \"@unikit-bridge/ui/primitives\";\nimport { cva } from \"class-variance-authority\";\n\nimport { AnchoredOverlay, type OverlayAlign, type OverlayPlacement } from \"../lib/overlay\";\nimport { cn } from \"@/lib/utils\";\n\ntype PopoverContextValue = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  /** 트리거 aria-controls ↔ 카드 nativeID 연결용 토대 ID */\n  baseId: string;\n  /** 트리거 요소 ref — 앵커 측정닫힘 후 포커스 복귀에 공용 */\n  triggerRef: MutableRefObject<BridgePressableRef | null>;\n  /** PopoverAnchor 오버라이드 ref — 있으면 이쪽이 앵커가 된다 */\n  anchorRef: MutableRefObject<BridgeViewRef | null>;\n  hasAnchor: boolean;\n  setHasAnchor: (has: boolean) => void;\n};\n\nconst PopoverContext = createContext<PopoverContextValue | null>(null);\n\nfunction usePopoverContext(component: string): PopoverContextValue {\n  const ctx = useContext(PopoverContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <Popover> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\n/** 외부 ref와 루트가 소유한 ref를 한 노드에 동시 연결한다. */\nfunction composeRefs<T>(\n  local: MutableRefObject<T | null>,\n  forwarded: ForwardedRef<T>,\n) {\n  return (node: T | null) => {\n    local.current = node;\n    if (typeof forwarded === \"function\") forwarded(node);\n    else if (forwarded) forwarded.current = node;\n  };\n}\n\nexport type PopoverProps = ViewProps & {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\nconst Popover = forwardRef<BridgeViewRef, PopoverProps>(\n  ({ open, defaultOpen, onOpenChange, children, ...props }, ref) => {\n    // 비제어 상태 — 제어（open 지정）면 무시된다.\n    const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);\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<BridgePressableRef | null>(null);\n    const anchorRef = useRef<BridgeViewRef | null>(null);\n    const [hasAnchor, setHasAnchor] = useState(false);\n    const contextValue = useMemo<PopoverContextValue>(\n      () => ({ open: isOpen, setOpen, baseId, triggerRef, anchorRef, hasAnchor, setHasAnchor }),\n      [isOpen, setOpen, baseId, hasAnchor],\n    );\n    return (\n      <PopoverContext.Provider value={contextValue}>\n        <View ref={ref} {...props}>{children}</View>\n      </PopoverContext.Provider>\n    );\n  },\n);\nPopover.displayName = \"Popover\";\n\nexport type PopoverTriggerProps = PressableProps;\n\nconst PopoverTrigger = forwardRef<BridgePressableRef, PopoverTriggerProps>(\n  ({ onPress, ...props }, ref) => {\n    const popover = usePopoverContext(\"PopoverTrigger\");\n    return (\n      <Pressable\n        ref={composeRefs(popover.triggerRef, ref)}\n        accessibilityRole=\"button\"\n        aria-expanded={popover.open}\n        aria-controls={`${popover.baseId}-popover-content`}\n        onPress={() => {\n          popover.setOpen(!popover.open);\n          onPress?.();\n        }}\n        {...props}\n      />\n    );\n  },\n);\nPopoverTrigger.displayName = \"PopoverTrigger\";\n\nconst popoverContentVariants = cva(\n  // pointer-events-auto: 웹에서 오버레이 래퍼 box-none 상속 회피（기본 포함）.\n  \"pointer-events-auto w-72 rounded-md border border-border bg-popover p-4 text-popover-foreground unikit-popover-in web:outline-none web:shadow-md\",\n);\n\nexport type PopoverContentProps = ViewProps & {\n  /** 전개 방향（기본 bottom） — 원본 side 프로퍼티와 동일 값 목록 */\n  side?: OverlayPlacement;\n  /** 앵커 변 기준 정렬（기본 center — 원본 기본값） */\n  align?: OverlayAlign;\n  /** 앵커와의 간격 px（기본 4 — 원본 기본값） */\n  sideOffset?: number;\n};\n\nconst PopoverContent = forwardRef<BridgeViewRef, PopoverContentProps>(\n  ({ className, side = \"bottom\", align = \"center\", sideOffset = 4, children, nativeID, ...props }, ref) => {\n    const popover = usePopoverContext(\"PopoverContent\");\n    return (\n      <AnchoredOverlay\n        visible={popover.open}\n        onClose={() => popover.setOpen(false)}\n        anchorRef={popover.hasAnchor ? popover.anchorRef : popover.triggerRef}\n        placement={side}\n        align={align}\n        offset={sideOffset}\n        triggerRef={popover.triggerRef}\n        className=\"z-50\"\n      >\n        <View\n          ref={ref}\n          accessibilityRole=\"dialog\"\n          nativeID={nativeID ?? `${popover.baseId}-popover-content`}\n          className={cn(popoverContentVariants(), className)}\n          {...props}\n        >\n          {children}\n        </View>\n      </AnchoredOverlay>\n    );\n  },\n);\nPopoverContent.displayName = \"PopoverContent\";\n\nexport type PopoverAnchorProps = ViewProps;\n\nconst PopoverAnchor = forwardRef<BridgeViewRef, PopoverAnchorProps>(\n  ({ children, ...props }, ref) => {\n    const { anchorRef, setHasAnchor } = usePopoverContext(\"PopoverAnchor\");\n    // 마운트 시 앵커 오버라이드 등록 — Content가 이 ref를 앵커로 삼는다.\n    // setHasAnchor（useState setter）는 안정 식별자라 1회 실행을 보장한다.\n    useEffect(() => {\n      setHasAnchor(true);\n      return () => setHasAnchor(false);\n    }, [setHasAnchor]);\n    return (\n      <View ref={composeRefs(anchorRef, ref)} {...props}>\n        {children}\n      </View>\n    );\n  },\n);\nPopoverAnchor.displayName = \"PopoverAnchor\";\n\nconst popoverHeaderVariants = cva(\"flex-col gap-1 text-sm\");\n\nexport type PopoverHeaderProps = ViewProps;\n\nconst PopoverHeader = forwardRef<BridgeViewRef, PopoverHeaderProps>(\n  ({ className, ...props }, ref) => (\n    <View ref={ref} className={cn(popoverHeaderVariants(), className)} {...props} />\n  ),\n);\nPopoverHeader.displayName = \"PopoverHeader\";\n\nconst popoverTitleVariants = cva(\"font-sans text-sm font-medium text-popover-foreground\");\n\nexport type PopoverTitleProps = ViewProps;\n\nconst PopoverTitle = forwardRef<BridgeViewRef, PopoverTitleProps>(\n  ({ className, children, ...props }, ref) => (\n    // 원본은 <div>（h2 역할 없음） — 구조를 유지하되 네이티브 색 상속 불가로\n    // 이중 지정한다（컨테이너 글자색은 웹 상속 전용）.\n    <View ref={ref} className={cn(popoverTitleVariants(), className)} {...props}>\n      <Text className=\"font-sans text-sm font-medium text-popover-foreground\">\n        {children}\n      </Text>\n    </View>\n  ),\n);\nPopoverTitle.displayName = \"PopoverTitle\";\n\nconst popoverDescriptionVariants = cva(\"text-muted-foreground\");\n\nexport type PopoverDescriptionProps = ViewProps;\n\nconst PopoverDescription = forwardRef<BridgeViewRef, PopoverDescriptionProps>(\n  ({ className, children, ...props }, ref) => (\n    <View ref={ref} className={cn(popoverDescriptionVariants(), className)} {...props}>\n      <Text className=\"font-sans text-sm text-muted-foreground\">{children}</Text>\n    </View>\n  ),\n);\nPopoverDescription.displayName = \"PopoverDescription\";\n\nexport {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  PopoverAnchor,\n  PopoverHeader,\n  PopoverTitle,\n  PopoverDescription,\n  popoverContentVariants,\n  popoverHeaderVariants,\n  popoverTitleVariants,\n  popoverDescriptionVariants,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/popover.tsx"
    }
  ],
  "type": "registry:ui"
}
