{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hover-card",
  "title": "HoverCard",
  "description": "shadcn/ui(new-york-v4) hover-card의 유니버설(웹+네이티브 한 벌 소스) 포팅. React state + context로, 오버레이는 lib/overlay의 AnchoredOverlay로 구성한다. 포팅 판단(원본 대비 차이):",
  "dependencies": [
    "@unikit-bridge/ui",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils",
    "animations"
  ],
  "files": [
    {
      "path": "registry/ui/hover-card.tsx",
      "content": "// HoverCard — shadcn/ui（new-york-v4） hover-card.tsx 유니버설 포팅 （사이클 40, T-1b）.\n// 원본: shadcn-ref/ui/hover-card.tsx — radix HoverCard 엔진을 제거하고 열림 상태는\n// React state + context로, 오버레이는 lib/overlay의 AnchoredOverlay로 구성한다.\n//\n// 포팅 판단（원본 대비 차이）:\n// - 진입 조건 설계: 원본은 포인터 진입（hover） 후 openDelay（ms） 뒤 열리고 포인터\n//   이탈 시 closeDelay 뒤 닫힌다. 브리지 PressableProps에 hover/포인터 이벤트\n//   계약이 없어（onLongPress는 웹 폐기onFocus 부재） hover 상태 열림을 포인터\n//   이벤트로 관리할 수 없다 — 진입을 press 토글로 근사한다.\n//    onPress（웹 클릭 + 네이티브 탭）: 열림/닫힘 토글 — 양측 공통 진입로.\n//    onLongPress（네이티브 롱프레스 전용, 웹은 브리지가 폐기）: 열림.\n//   플랫폼 분기 없이 두 핸들러를 항상 연결해 어느 쪽에서도 동작한다.\n// - openDelay（원본 기본 200）closeDelay（원본 기본 300） 프로퍼티는 API 호환을\n//   위해 수용하되 즉시 전환 근사에서는 대기 없이 바로 열림/닫힘（미사용 토큰）.\n// - 콘텐츠 이탈 닫힘바깥 포인터 닫힘 미지원（포인터 이벤트 계약 부재\n//   AnchoredOverlay 비모달 원칙） — 닫기는 ESC（브리지 useDismiss, 웹）와\n//   트리거 재누름으로 한다. openDelay/closeDelay와 함께 원본 대비 상실 목록.\n// - 원본 Content의 sidesideOffset 프로퍼티명을 유지하되 내부적으로\n//   AnchoredOverlay의 placementoffset로 매핑（기본 side \"bottom\"sideOffset 4\n//   align \"center\"는 원본 기본값 그대로）.\n// - 애니메이션（animate-in/outfadezoomdata-［side］ 슬라이드）과 origin 변형은\n//   즉시 전환으로 근사 — 토큰 전부 제거.\n// - asChild 미지원 — 트리거는 항상 브리지 Pressable（웹 <button>）로 렌더된다.\n// - 카드 className에 pointer-events-auto 기본 포함（웹 box-none 상속 회피）.\n// - 접근성: 트리거 aria-expandedaria-controls（useId 기반 nativeID）. 원본 radix\n//   HoverCard Content는 별도 role을 노출하지 않아 이식도 카드에 role을 부여하지\n//   않는다. RN은 aria-expanded만 accessibilityState.expanded로 치환되고\n//   aria-controls는 무시된다（브리지 계약 문서화된 한계）.\n// - dark: 변형 토큰은 프로젝트 관행（미사용）대로 제거.\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ForwardedRef,\n  type MutableRefObject,\n} from \"react\";\nimport {\n  Pressable,\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 HoverCardContextValue = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  /** 트리거 aria-controls ↔ 카드 nativeID 연결용 토대 ID */\n  baseId: string;\n  /** 트리거 요소 ref — 앵커 측정닫힘 후 포커스 복귀에 공용 */\n  triggerRef: MutableRefObject<BridgePressableRef | null>;\n};\n\nconst HoverCardContext = createContext<HoverCardContextValue | null>(null);\n\nfunction useHoverCardContext(component: string): HoverCardContextValue {\n  const ctx = useContext(HoverCardContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <HoverCard> 안에서 사용해야 합니다.`);\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 HoverCardProps = ViewProps & {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** 원본 radix openDelay（기본 200） 호환 — 즉시 전환 근사에서 미사용 */\n  openDelay?: number;\n  /** 원본 radix closeDelay（기본 300） 호환 — 즉시 전환 근사에서 미사용 */\n  closeDelay?: number;\n};\n\nconst HoverCard = forwardRef<BridgeViewRef, HoverCardProps>(\n  (\n    // openDelaycloseDelay는 API 호환용으로만 수용（즉시 전환 근사에서 미사용） —\n    // rest로 흘러 View에 알 수 없는 속성이 되지 않게 폐기한다（브리지 관행）.\n    { open, defaultOpen, onOpenChange, openDelay: _openDelay, closeDelay: _closeDelay, children, ...props },\n    ref,\n  ) => {\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 contextValue = useMemo<HoverCardContextValue>(\n      () => ({ open: isOpen, setOpen, baseId, triggerRef }),\n      [isOpen, setOpen, baseId],\n    );\n    return (\n      <HoverCardContext.Provider value={contextValue}>\n        <View ref={ref} {...props}>{children}</View>\n      </HoverCardContext.Provider>\n    );\n  },\n);\nHoverCard.displayName = \"HoverCard\";\n\nexport type HoverCardTriggerProps = PressableProps;\n\nconst HoverCardTrigger = forwardRef<BridgePressableRef, HoverCardTriggerProps>(\n  ({ onPress, onLongPress, ...props }, ref) => {\n    const hoverCard = useHoverCardContext(\"HoverCardTrigger\");\n    return (\n      <Pressable\n        ref={composeRefs(hoverCard.triggerRef, ref)}\n        accessibilityRole=\"button\"\n        aria-expanded={hoverCard.open}\n        aria-controls={`${hoverCard.baseId}-hover-card-content`}\n        onPress={() => {\n          hoverCard.setOpen(!hoverCard.open);\n          onPress?.();\n        }}\n        onLongPress={() => {\n          hoverCard.setOpen(true);\n          onLongPress?.();\n        }}\n        {...props}\n      />\n    );\n  },\n);\nHoverCardTrigger.displayName = \"HoverCardTrigger\";\n\nconst hoverCardContentVariants = cva(\n  // pointer-events-auto: 웹에서 오버레이 래퍼 box-none 상속 회피（기본 포함）.\n  \"pointer-events-auto w-64 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 HoverCardContentProps = ViewProps & {\n  /** 전개 방향（기본 bottom） — 원본 side 프로퍼티와 동일 값 목록 */\n  side?: OverlayPlacement;\n  /** 앵커 변 기준 정렬（기본 center — 원본 기본값） */\n  align?: OverlayAlign;\n  /** 앵커와의 간격 px（기본 4 — 원본 기본값） */\n  sideOffset?: number;\n};\n\nconst HoverCardContent = forwardRef<BridgeViewRef, HoverCardContentProps>(\n  (\n    { className, side = \"bottom\", align = \"center\", sideOffset = 4, children, nativeID, ...props },\n    ref,\n  ) => {\n    const hoverCard = useHoverCardContext(\"HoverCardContent\");\n    return (\n      <AnchoredOverlay\n        visible={hoverCard.open}\n        onClose={() => hoverCard.setOpen(false)}\n        anchorRef={hoverCard.triggerRef}\n        placement={side}\n        align={align}\n        offset={sideOffset}\n        triggerRef={hoverCard.triggerRef}\n        className=\"z-50\"\n      >\n        <View\n          ref={ref}\n          nativeID={nativeID ?? `${hoverCard.baseId}-hover-card-content`}\n          className={cn(hoverCardContentVariants(), className)}\n          {...props}\n        >\n          {children}\n        </View>\n      </AnchoredOverlay>\n    );\n  },\n);\nHoverCardContent.displayName = \"HoverCardContent\";\n\nexport {\n  HoverCard,\n  HoverCardTrigger,\n  HoverCardContent,\n  hoverCardContentVariants,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/hover-card.tsx"
    }
  ],
  "type": "registry:ui"
}
