{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-otp",
  "title": "InputOtp",
  "description": "shadcn/ui(new-york-v4) input-otp의 유니버설(웹+네이티브 한 벌 소스) 포팅. OTPInputContext)를 제거하고 단일 숨은 TextInput 방식으로 직접 구현한다. 포팅 판단(원본 대비 차이):",
  "dependencies": [
    "@unikit-bridge/ui",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/input-otp.tsx",
      "content": "// InputOTP — shadcn/ui（new-york-v4） input-otp.tsx 유니버설 포팅 （사이클 40, T-2a）.\n// 원본: shadcn-ref/ui/input-otp.tsx — input-otp 라이브러리（OTPInput\n// OTPInputContext）를 제거하고 단일 숨은 TextInput 방식으로 직접 구현한다.\n//\n// 포팅 판단（원본 대비 차이）:\n// - 구현 방식 선택: 셀별 입력（셋당 TextInput） 대신 단일 숨은 입력 — 셀 개수\n//   maxLength의 투명 TextInput을 셀 위에 absolute로 겹치고 셀은 표시 전용\n//   View로 그린다. 이유: （1） 네이티브에서 셀별 입력은 maxLength=1 때문에 붙여넣기\n//   SMS 자동입력이 잘리는데 반해 단일 입력은 양측에서 자연 분배되고, （2） 백스페이스\n//   화살표캐럿 등 키보드 조작을 입력 요소가 자체 처리해 셀 간 포커스 이동\n//   로직（ref focus）이 불필요하다. 투명 요소도 터치를 받으므로（양측 확인） 셀 어디를\n//   눌러도 입력이 포커스된다.\n// - 원본 OTPInputContext（{ slots: ［{ char, hasFakeCaret, isActive }］ }）와 동일한\n//   context를 제공해 InputOTPSlot 구조를 원본에 최대한 맞췄다.\n// - 활성 셀/가짜 캐럿: 브리지 TextInputProps에 onFocus/onBlur 계약이 없어 포커스\n//   이벤트를 알 수 없다. 대신 \"삽입 위치 모델\"로 근사 — 첫 빈 셀（모두 찼으면 마지막\n//   셀）을 활성으로 표시한다. 미포커스 상태에도 캐럿이 보일 수 있다（한계）.\n// - 캐럿 깜빡임（animate-caret-blink）: 웹은 keyframes가 하드 온오프로 깜빡이고\n//   네이티브는 브리지 LoopPulse（RN Animated）로 부드러운 맥동 근사（주기 1.25s\n//   준용 — 네이티브 근사 차이）.\n// - 슬롯의 first:/last: 위치 셀렉터는 uniwind 미지원（확인） — 인덱스 0/끝이 곧\n//   첫/마지막 셀이라는 성질（input-otp 슬롯 인덱스는 전역 연속）로 조건부 클래스\n//   전개（rounded-l-md border-l / rounded-r-md）.\n// - aria-invalid:* 셀렉터 스타일은 invalid?: boolean prop（textarea.tsx 관행）으로\n//   받아 context 전파 + 조건부 클래스로 치환. ring-［3px］→ring-3（비대괄호 계단 —\n//   사이클 72 원본 규격 정렬）, dark: 제거.\n// - 구분선 아이콘은 브리지 SVG MinusIcon 프리미티브를 그대로 쓴다.\n//   autoComplete=\"one-time-code\"（SMS 자동입력 힌트）는\n//   브리지 계약에 없어 미지원（로드맵）.\n// - 데이터 규칙: 입력 문자를 걸러내지 않는다（원본 기본 동일）, value는\n//   maxLength로 절단된 문자열을 유지한다.\nimport { createContext, forwardRef, useContext, useState } from \"react\";\nimport {\n  LoopPulse,\n  MinusIcon,\n  Text,\n  TextInput,\n  View,\n  type BridgeTextInputRef,\n  type BridgeViewRef,\n  type TextInputProps,\n  type ViewProps,\n} from \"@unikit-bridge/ui/primitives\";\nimport { cva } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\n// 원본 input-otp의 OTPInputContext 슬롯 상태와 동일 형태.\ntype OTPSlotState = {\n  char?: string;\n  hasFakeCaret: boolean;\n  isActive: boolean;\n};\n\ntype InputOTPContextValue = {\n  slots: OTPSlotState[];\n  invalid: boolean;\n};\n\nconst InputOTPContext = createContext<InputOTPContextValue | null>(null);\n\nexport type InputOTPProps = ViewProps & {\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  /** 셀 개수（원본 OTPInput maxLength） */\n  maxLength: number;\n  /** 모든 셀이 채워졌을 때 1회 발화 */\n  onComplete?: (value: string) => void;\n  disabled?: boolean;\n  autoFocus?: boolean;\n  inputMode?: TextInputProps[\"inputMode\"];\n  /** 숨은 입력 ↔ Label 연결（웹 aria-labelledby — bridge/types.ts 계약） */\n  accessibilityLabelledBy?: string;\n  /** 오류 상태（원본 aria-invalid:* 치환） — 셀 테두리링 */\n  invalid?: boolean;\n  /** 컨테이너（셀 래퍼） 클래스 — 원본 containerClassName */\n  containerClassName?: string;\n};\n\nconst InputOTP = forwardRef<BridgeTextInputRef, InputOTPProps>(\n  (\n    {\n      className,\n      style,\n      containerClassName,\n      value,\n      defaultValue,\n      onChange,\n      onComplete,\n      maxLength,\n      disabled,\n      autoFocus,\n      inputMode,\n      invalid,\n      nativeID,\n      testID,\n      accessibilityLabelledBy,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const cellCount = Math.max(1, maxLength);\n    // 비제어 상태 — 제어（value 지정）면 무시된다.\n    const [internalValue, setInternalValue] = useState(defaultValue ?? \"\");\n    const current = value ?? internalValue;\n    const commit = (text: string) => {\n      const next = text.slice(0, cellCount);\n      if (value === undefined) setInternalValue(next);\n      if (next !== current) {\n        onChange?.(next);\n        if (next.length === cellCount) onComplete?.(next);\n      }\n    };\n    // 삽입 위치 모델 — 첫 빈 셀, 모두 찼으면 마지막 셀（머리 주석）.\n    const insertion = Math.min(current.length, cellCount - 1);\n    const slots = Array.from({ length: cellCount }, (_, index) => ({\n      char: current[index],\n      isActive: index === insertion,\n      hasFakeCaret: index === insertion && !current[index] && !disabled,\n    }));\n    return (\n      <InputOTPContext.Provider\n        value={{ slots, invalid: invalid ?? false }}\n      >\n        <View\n          className={cn(\n            // 원본 루트 flex items-center gap-2 — 구분선(Separator)과 셀 그룹 사이 간격.\n            \"relative flex-row items-center gap-2\",\n            disabled && \"opacity-50\",\n            containerClassName,\n          )}\n          style={style}\n          {...props}\n        >\n          {children}\n          {/* 숨은 입력 — 셀 위를 덮는 투명 오버레이. 자식 이후에 렌더해 양측에서\n              최상단(터치 수신)이 되게 한다. 웹 caret도 opacity로 가려진다. */}\n          <TextInput\n            ref={ref}\n            className={cn(\n              \"absolute inset-0 bg-transparent opacity-0 web:cursor-default\",\n              className,\n            )}\n            value={current}\n            onChangeText={commit}\n            maxLength={cellCount}\n            editable={disabled ? false : undefined}\n            disabled={disabled}\n            autoFocus={autoFocus}\n            inputMode={inputMode}\n            nativeID={nativeID}\n            testID={testID}\n            accessibilityLabelledBy={accessibilityLabelledBy}\n            aria-invalid={invalid ? true : undefined}\n          />\n        </View>\n      </InputOTPContext.Provider>\n    );\n  },\n);\nInputOTP.displayName = \"InputOTP\";\n\nconst inputOTPGroupVariants = cva(\"flex-row items-center\");\n\nexport type InputOTPGroupProps = ViewProps;\n\nconst InputOTPGroup = forwardRef<BridgeViewRef, InputOTPGroupProps>(\n  ({ className, ...props }, ref) => (\n    <View\n      ref={ref}\n      className={cn(inputOTPGroupVariants(), className)}\n      {...props}\n    />\n  ),\n);\nInputOTPGroup.displayName = \"InputOTPGroup\";\n\nexport type InputOTPSlotProps = ViewProps & {\n  index: number;\n};\n\nconst InputOTPSlot = forwardRef<BridgeViewRef, InputOTPSlotProps>(\n  ({ index, className, ...props }, ref) => {\n    // InputOTP 밖 사용 시 조용히 빈 셀（원본의 optional chaining 관행과 동일）.\n    const otp = useContext(InputOTPContext);\n    const { char, hasFakeCaret, isActive } = otp?.slots[index] ?? {};\n    const cellCount = otp?.slots.length ?? 0;\n    return (\n      <View\n        ref={ref}\n        className={cn(\n          \"relative flex h-9 w-9 items-center justify-center border-y border-r border-input web:transition-all web:outline-none\",\n          // first:/last: 근사 — 인덱스로 전개（머리 주석）.\n          index === 0 && \"rounded-l-md border-l\",\n          cellCount > 0 && index === cellCount - 1 && \"rounded-r-md\",\n          isActive && \"z-10 border-ring ring-3 ring-ring/50\",\n          otp?.invalid && \"border-destructive\",\n          isActive && otp?.invalid && \"ring-destructive/20\",\n          className,\n        )}\n        {...props}\n      >\n        {char ? (\n          <Text className=\"font-sans text-sm text-foreground\">{char}</Text>\n        ) : null}\n        {hasFakeCaret && (\n          <View\n            aria-hidden\n            className=\"absolute inset-0 items-center justify-center\"\n          >\n            {/* 가짜 캐럿 — 웹은 animate-caret-blink(하드 깜빡임), 네이티브는\n                LoopPulse(부드러운 맥동)로 근사（원본 1.25s 주기 준용） */}\n            <LoopPulse durationMs={1250}>\n              <View className=\"h-4 w-px bg-foreground\" />\n            </LoopPulse>\n          </View>\n        )}\n      </View>\n    );\n  },\n);\nInputOTPSlot.displayName = \"InputOTPSlot\";\n\nexport type InputOTPSeparatorProps = ViewProps;\n\nconst InputOTPSeparator = forwardRef<BridgeViewRef, InputOTPSeparatorProps>(\n  ({ className, ...props }, ref) => (\n    <View\n      ref={ref}\n      accessibilityRole=\"separator\"\n      className={cn(\"flex-row items-center\", className)}\n      {...props}\n    >\n      <MinusIcon className=\"size-4 text-muted-foreground web:select-none\" />\n    </View>\n  ),\n);\nInputOTPSeparator.displayName = \"InputOTPSeparator\";\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };\n",
      "type": "registry:ui",
      "target": "components/ui/input-otp.tsx"
    }
  ],
  "type": "registry:ui"
}
