{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select",
  "title": "Select",
  "description": "shadcn/ui(new-york-v4) select의 유니버설(웹+네이티브 한 벌 소스) 포팅. (완성된 선행 컴포넌트, 수정 금지)의 Popover/PopoverTrigger/PopoverContent를 조합해 직접 구현한다. 열림은 Popover(제어형 open)이, 선택 값은 이 파일의",
  "dependencies": [
    "@unikit-bridge/ui",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "popover",
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/select.tsx",
      "content": "// Select — shadcn/ui（new-york-v4） select.tsx 유니버설 포팅 （사이클 40, T-2a）.\n// 원본: shadcn-ref/ui/select.tsx — radix Select 엔진을 제거하고 popover.tsx\n// （완성된 선행 컴포넌트, 수정 금지）의 Popover/PopoverTrigger/PopoverContent를\n// 조합해 직접 구현한다. 열림은 Popover（제어형 open）이, 선택 값은 이 파일의\n// React state + context가 관리한다.\n//\n// 포팅 판단（원본 대비 차이）:\n// - radix Root/Trigger/Content를 Select（값 context + Popover 래핑）SelectTrigger\n//   （PopoverTrigger + combobox role）SelectContent（PopoverContent + ScrollView\n//   목록）로 재구성. 원본 10종 export（API）는 전량 유지한다.\n// - 원본 SelectValue는 닫힌 상태에서도 항목 라벨을 보여준다（radix가 내부 캐시\n//   유지）. 포팅은 콘텐츠가 언마운트되므로 2단 조회로 근사: （1） 루트가 children\n//   정적 트리를 순회해 SelectItem（value→라벨）을 수집 — 초기/defaultValue 표시\n//   대응, （2） SelectItem이 마운트될 때마다 라벨을 context에 등록（동적 목록요소\n//   children 대응）. 라벨을 문자열로 얻을 수 없으면 value 문자열을 보여준다（한계）.\n// - position（\"item-aligned\"|\"popper\"）은 시그니처만 유지 — radix의 선택 항목\n//   정렬팝퍼 오프셋은 재현 불가, AnchoredOverlay의 앵커 배치로 근사된다.\n// - SelectScrollUpButton/DownButton은 API 유지용 표시 컴포넌트로만 export —\n//   radix의 오버플로우 감지 자동 표시는 불가해 SelectContent가 기본 렌더하지\n//   않는다（목록은 ScrollView로 터치/휠 스크롤）.\n// - 아이콘: ChevronDownIcon（트리거）CheckIcon（항목 표시자）ChevronUpIcon/\n//   DownIcon（스크롤 버튼） — 브리지 lucide 아이콘 프리미티브（dropdown-menu 관행）.\n// - 원본의 data-［state］data-［placeholder］*:data-［slot=］［&_svg］ 셀렉터 클래스는\n//   조건부 클래스내부 Text 지정으로 전개（data 속성 부재）.\n// - aria-invalid:* 트리거 토큰은 PressableProps의 aria-invalid 전달（웹 DOM）과\n//   web:aria-invalid: 클래스로 유지.\n// - 임의값 근사: min-w-［8rem］→min-w-32（동일 8rem）, ring-［3px］→ring-3（비대괄호\n//   계단 — 사이클 72 원본 규격 정렬）, max-h-（--radix-）→max-h-96,\n//   transition-［］→web:transition-all. dark: 제거.\n// - 미지원（로드맵）: 화살표키타입어헤드 탐색, 등장/퇴장 애니메이션, name/required\n//   폼 제출용 숨은 input, 항목 textValue 프로퍼티, SelectValue 함수형 children.\n// - 접근성: 트리거 combobox role + popover가 부여하는 aria-expandedaria-controls\n//   （↔ 콘텐츠 nativeID）, 콘텐츠 listbox role, 항목 option + aria-selected.\n//   RN은 combobox를 accessibilityState.expanded로 일부 반영하며 listbox/option\n//   role은 카탈로그에 없어 무시될 수 있다（브리지 계약 문서화 방침）.\nimport {\n  Children,\n  createContext,\n  forwardRef,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport {\n  CheckIcon,\n  ChevronDownIcon,\n  ChevronUpIcon,\n  Pressable,\n  ScrollView,\n  Text,\n  View,\n  type BridgeKeyEvent,\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 { Popover, PopoverContent, PopoverTrigger } from \"./popover\";\nimport type { OverlayAlign, OverlayPlacement } from \"../lib/overlay\";\nimport { cn } from \"@/lib/utils\";\n\n// ---------------------------------------------------------------------------\n// 내부 유틸\n// ---------------------------------------------------------------------------\n\n/** 문자열/숫자 자식만 이어 붙여 라벨을 뽑는다 — 요소 자식은 undefined（한계）. */\nfunction textOfChildren(children: ReactNode): string | undefined {\n  const parts = Children.toArray(children)\n    .map((child) =>\n      typeof child === \"string\" || typeof child === \"number\"\n        ? String(child)\n        : undefined,\n    )\n    .filter((part): part is string => part !== undefined);\n  return parts.length > 0 ? parts.join(\"\") : undefined;\n}\n\n/**\n * 자식 중 문자열/숫자를 내부 Text로 감싼다 — 네이티브는 Text 없이 문자열을\n * 렌더할 수 없고 색 상속도 되지 않는다（이중 지정）. 요소 자식은 그대로 둔다.\n */\nfunction wrapTextChildren(children: ReactNode, textClassName: string) {\n  return Children.map(children, (child) =>\n    typeof child === \"string\" || typeof child === \"number\" ? (\n      <Text className={textClassName}>{child}</Text>\n    ) : (\n      child\n    ),\n  );\n}\n\n/**\n * children 정적 트리에서 SelectItem（value→라벨）을 수집한다 — 콘텐츠가 언마운트된\n * 닫힘 상태에서 SelectValue가 라벨을 보여주기 위한 1단 조회（머리 주석）.\n * SelectContentSelectGroup 컨테이너만 들어가고, 그 외 래퍼는 관통하지 않는다.\n */\nfunction collectItemLabels(\n  children: ReactNode,\n  labels: Record<string, string>,\n): void {\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) return;\n    const props = child.props as { value?: string; children?: ReactNode };\n    if (child.type === SelectItem) {\n      if (typeof props.value === \"string\") {\n        const label = textOfChildren(props.children);\n        if (label !== undefined) labels[props.value] = label;\n      }\n      return;\n    }\n    if (child.type === SelectContent || child.type === SelectGroup) {\n      collectItemLabels(props.children, labels);\n    }\n  });\n}\n\n// ---------------------------------------------------------------------------\n// 값 상태 context\n// ---------------------------------------------------------------------------\n\ntype SelectContextValue = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  value: string;\n  /** 항목 선택 확정 — 값 반영 + popover 닫음 */\n  setValue: (value: string) => void;\n  /** 루트 disabled — 트리거에 전파（radix Root disabled 관행） */\n  disabled: boolean;\n  /** 열린 콘텐츠에서 항목이 자기 라벨을 등록（2단 조회 — 동적 목록 대응） */\n  registerLabel: (value: string, label: string) => void;\n  /** value → 표시 라벨（등록값 → 정적 수집값 → value 문자열 폴백） */\n  labelOf: (value: string) => string;\n};\n\nconst SelectContext = createContext<SelectContextValue | null>(null);\n\nfunction useSelectContext(component: string): SelectContextValue {\n  const ctx = useContext(SelectContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <Select> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\n// ---------------------------------------------------------------------------\n// 컴포넌트\n// ---------------------------------------------------------------------------\n\nexport type SelectProps = ViewProps & {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** 트리거 비활성화（radix Root disabled） */\n  disabled?: boolean;\n};\n\nconst Select = forwardRef<BridgeViewRef, SelectProps>(\n  (\n    {\n      value,\n      defaultValue,\n      onValueChange,\n      open,\n      defaultOpen,\n      onOpenChange,\n      disabled,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    // 비제어 상태 — 제어（값열림 지정）면 무시된다.\n    const [internalValue, setInternalValue] = useState(defaultValue ?? \"\");\n    const current = value ?? internalValue;\n    const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);\n    const isOpen = open ?? internalOpen;\n    const stateRef = useRef({ open, isOpen, onOpenChange, value, current, onValueChange });\n    stateRef.current = { open, isOpen, onOpenChange, value, current, onValueChange };\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 setValue = useCallback(\n      (next: string) => {\n        const state = stateRef.current;\n        if (state.value === undefined) setInternalValue(next);\n        if (next !== state.current) state.onValueChange?.(next);\n        setOpen(false);\n      },\n      [setOpen],\n    );\n\n    // 라벨 2단 조회 상태 — 정적 수집（트리 순회） + 항목 마운트 등록.\n    const [runtimeLabels, setRuntimeLabels] = useState<\n      Record<string, string>\n    >({});\n    const registerLabel = useCallback((itemValue: string, label: string) => {\n      setRuntimeLabels((prev) =>\n        prev[itemValue] === label\n          ? prev\n          : { ...prev, [itemValue]: label },\n      );\n    }, []);\n    const staticLabels = useMemo(() => {\n      const labels: Record<string, string> = {};\n      collectItemLabels(children, labels);\n      return labels;\n    }, [children]);\n    const labelOf = useCallback(\n      (itemValue: string) =>\n        runtimeLabels[itemValue] ?? staticLabels[itemValue] ?? itemValue,\n      [runtimeLabels, staticLabels],\n    );\n\n    const contextValue = useMemo<SelectContextValue>(\n      () => ({\n        open: isOpen,\n        setOpen,\n        value: current,\n        setValue,\n        disabled: disabled ?? false,\n        registerLabel,\n        labelOf,\n      }),\n      [isOpen, setOpen, current, setValue, disabled, registerLabel, labelOf],\n    );\n\n    return (\n      <Popover ref={ref} open={isOpen} onOpenChange={setOpen} {...props}>\n        <SelectContext.Provider value={contextValue}>\n          {children}\n        </SelectContext.Provider>\n      </Popover>\n    );\n  },\n);\nSelect.displayName = \"Select\";\n\n// 트리거 — 원본 토큰에서 data-size/셀렉터류를 cva size조건부 클래스로 전개.\n// w-fit → self-start（tabs.tsx 관행）, focus 링은 원본 규격（ring-3 무오프셋） 준수.\nconst selectTriggerVariants = cva(\n  \"flex-row items-center justify-between gap-2 self-start rounded-md border border-input bg-transparent px-3 py-2 web:whitespace-nowrap web:transition-all web:outline-none web:focus-visible:border-ring web:focus-visible:ring-3 web:focus-visible:ring-ring/50 web:aria-invalid:border-destructive web:aria-invalid:ring-destructive/20\",\n  {\n    variants: {\n      size: {\n        default: \"h-9\",\n        sm: \"h-8\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  },\n);\n\nexport type SelectTriggerProps = PressableProps & {\n  size?: \"sm\" | \"default\";\n};\n\nconst SelectTrigger = forwardRef<BridgePressableRef, SelectTriggerProps>(\n  ({ className, size = \"default\", children, disabled, onKeyDown, ...props }, ref) => {\n    const select = useSelectContext(\"SelectTrigger\");\n    // radix 관행 — 루트 disabled와 트리거 disabled는 OR로 합쳐진다.\n    const isDisabled = disabled || select.disabled;\n    const handleKeyDown = (event: BridgeKeyEvent) => {\n      if (!isDisabled && (event.key === \"ArrowDown\" || event.key === \"ArrowUp\")) {\n        event.preventDefault();\n        if (!select.open) select.setOpen(true);\n      }\n      onKeyDown?.(event);\n    };\n    return (\n      <PopoverTrigger\n        ref={ref}\n        accessibilityRole=\"combobox\"\n        disabled={isDisabled}\n        onKeyDown={handleKeyDown}\n        className={cn(\n          selectTriggerVariants({ size }),\n          isDisabled && \"web:cursor-not-allowed opacity-50\",\n          className,\n        )}\n        {...props}\n      >\n        {wrapTextChildren(children, \"font-sans text-sm text-foreground\")}\n        <ChevronDownIcon className=\"size-4 shrink-0 text-muted-foreground opacity-50 web:select-none\" />\n      </PopoverTrigger>\n    );\n  },\n);\nSelectTrigger.displayName = \"SelectTrigger\";\n\nexport type SelectValueProps = TextProps & {\n  placeholder?: ReactNode;\n};\n\nconst SelectValue = forwardRef<BridgeTextRef, SelectValueProps>(\n  ({ className, placeholder, children, ...props }, ref) => {\n    const select = useSelectContext(\"SelectValue\");\n    const hasValue = select.value !== \"\";\n    return (\n      // 원본 data-［placeholder］:text-muted-foreground（트리거）를 여기서 조건부로.\n      <Text\n        ref={ref}\n        className={cn(\n          \"font-sans text-sm text-foreground web:line-clamp-1\",\n          !hasValue && \"text-muted-foreground\",\n          className,\n        )}\n        {...props}\n      >\n        {hasValue ? (children ?? select.labelOf(select.value)) : placeholder}\n      </Text>\n    );\n  },\n);\nSelectValue.displayName = \"SelectValue\";\n\nexport type SelectContentProps = ViewProps & {\n  /** 원본 시그니처 유지 — item-aligned/popper 구분 없이 앵커 배치로 근사（무시） */\n  position?: \"item-aligned\" | \"popper\";\n  /** 트리거 기준 전개 방향（기본 bottom — popover 기본값） */\n  side?: OverlayPlacement;\n  /** 트리거 변 기준 정렬（기본 center — 원본 기본값） */\n  align?: OverlayAlign;\n  /** 트리거와의 간격 px（기본 4 — popover 기본값） */\n  sideOffset?: number;\n};\n\nconst SelectContent = forwardRef<BridgeViewRef, SelectContentProps>(\n  (\n    {\n      className,\n      children,\n      position: _position,\n      side,\n      align,\n      sideOffset,\n      nativeID,\n      ...props\n    },\n    ref,\n  ) => {\n    return (\n      <PopoverContent\n        ref={ref}\n        side={side}\n        align={align}\n        sideOffset={sideOffset}\n        // radix Select Content의 role（listbox） 치환 — popover 기본 dialog를\n        // 덮는다. nativeID는 popover 기본값（트리거 aria-controls와 연결됨） 유지.\n        accessibilityRole=\"listbox\"\n        nativeID={nativeID}\n        // popover 카드 기본 w-72 p-4를 셀렉트 목록용으로 무효화（twMerge 병합）\n        className={cn(\"w-auto min-w-32 p-0 web:shadow-md\", className)}\n        {...props}\n      >\n        {/* 목록 뷰포트 — 원본 Viewport p-1. ScrollView로 양측 실제 스크롤\n            (웹 overflow div ... 네이티브 RN ScrollView). max-h는 원본\n            --radix-...available-height의 표준 계단 근사. */}\n        <ScrollView className=\"max-h-96\" contentContainerClassName=\"p-1\">\n          {children}\n        </ScrollView>\n      </PopoverContent>\n    );\n  },\n);\nSelectContent.displayName = \"SelectContent\";\n\nexport type SelectGroupProps = ViewProps;\n\nconst SelectGroup = forwardRef<BridgeViewRef, SelectGroupProps>(\n  ({ ...props }, ref) => <View ref={ref} {...props} />,\n);\nSelectGroup.displayName = \"SelectGroup\";\n\nexport type SelectLabelProps = ViewProps;\n\nconst SelectLabel = forwardRef<BridgeViewRef, SelectLabelProps>(\n  ({ className, children, ...props }, ref) => (\n    <View ref={ref} className={cn(\"px-2 py-1.5\", className)} {...props}>\n      {/* 이중 지정: 전경색은 내부 Text에（네이티브 색 상속 불가） */}\n      <Text className=\"font-sans text-xs text-muted-foreground\">\n        {children}\n      </Text>\n    </View>\n  ),\n);\nSelectLabel.displayName = \"SelectLabel\";\n\nexport type SelectItemProps = Omit<\n  PressableProps,\n  \"onPress\" | \"aria-selected\"\n> & {\n  value: string;\n  /** 선택 콜백 — 선택 후 popover가 닫힌다（원본 preventDefault 닫기 방지 미지원） */\n  onSelect?: () => void;\n};\n\nconst SelectItem = forwardRef<BridgePressableRef, SelectItemProps>(\n  ({ className, value, disabled, onSelect, children, ...props }, ref) => {\n    const select = useSelectContext(\"SelectItem\");\n    const selected = select.value === value;\n    const label = textOfChildren(children);\n    // 열린 콘텐츠 안에서 라벨 등록 — 동적 목록정적 수집 놓침 보완（머리 주석）.\n    useEffect(() => {\n      if (label !== undefined) select.registerLabel(value, label);\n    }, [select, value, label]);\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"option\"\n        aria-selected={selected}\n        disabled={disabled}\n        onPress={() => {\n          select.setValue(value);\n          onSelect?.();\n        }}\n        className={cn(\n          \"relative flex-row w-full items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 active:bg-accent web:cursor-default web:select-none web:outline-none web:hover:bg-accent web:focus-visible:bg-accent\",\n          disabled && \"web:pointer-events-none opacity-50\",\n          className,\n        )}\n        {...props}\n      >\n        {/* 표시자 — 원본 ItemIndicator + CheckIcon（원본 슬롯 size-3.5·아이콘 size-4,\n            아이콘 색은 항목 하이라이트 전경을 따른다） */}\n        <View\n          aria-hidden\n          className=\"absolute right-2 size-3.5 items-center justify-center web:pointer-events-none\"\n        >\n          {selected ? (\n            <CheckIcon className=\"size-4 text-foreground active:text-accent-foreground web:hover:text-accent-foreground web:focus-visible:text-accent-foreground\" />\n          ) : null}\n        </View>\n        {wrapTextChildren(\n          children,\n          \"flex-1 font-sans text-sm text-popover-foreground active:text-accent-foreground web:hover:text-accent-foreground web:focus-visible:text-accent-foreground\",\n        )}\n      </Pressable>\n    );\n  },\n);\nSelectItem.displayName = \"SelectItem\";\n\nexport type SelectSeparatorProps = ViewProps;\n\nconst SelectSeparator = forwardRef<BridgeViewRef, SelectSeparatorProps>(\n  ({ className, ...props }, ref) => (\n    <View\n      ref={ref}\n      accessibilityRole=\"separator\"\n      className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n      {...props}\n    />\n  ),\n);\nSelectSeparator.displayName = \"SelectSeparator\";\n\nexport type SelectScrollButtonProps = ViewProps;\n\n// API 유지용 표시 컴포넌트 — SelectContent가 기본 렌더하지 않는다（머리 주석）.\nconst SelectScrollUpButton = forwardRef<\n  BridgeViewRef,\n  SelectScrollButtonProps\n>(({ className, ...props }, ref) => (\n  <View\n    ref={ref}\n    className={cn(\"flex-row items-center justify-center py-1\", className)}\n    {...props}\n  >\n    <ChevronUpIcon className=\"size-4 text-muted-foreground\" />\n  </View>\n));\nSelectScrollUpButton.displayName = \"SelectScrollUpButton\";\n\nconst SelectScrollDownButton = forwardRef<\n  BridgeViewRef,\n  SelectScrollButtonProps\n>(({ className, ...props }, ref) => (\n  <View\n    ref={ref}\n    className={cn(\"flex-row items-center justify-center py-1\", className)}\n    {...props}\n  >\n    <ChevronDownIcon className=\"size-4 text-muted-foreground\" />\n  </View>\n));\nSelectScrollDownButton.displayName = \"SelectScrollDownButton\";\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/select.tsx"
    }
  ],
  "type": "registry:ui"
}
