{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-otp",
  "type": "registry:ui",
  "title": "Input OTP",
  "description": "One-time password input for Tile UI.",
  "devDependencies": [
    "sass"
  ],
  "registryDependencies": [
    "@tile-ui/core",
    "@tile-ui/styles"
  ],
  "files": [
    {
      "path": "packages/react/src/components/input-otp/input-otp.tsx",
      "type": "registry:ui",
      "target": "components/ui/input-otp/input-otp.tsx",
      "content": "import React, { createContext, useCallback, useContext, useRef, useState } from 'react';\nimport { getNextOtpIndex, getPrevOtpIndex, inputOtpStyleKeys, isOtpCharAllowed, joinOtpValue, splitOtpValue } from '../lib/core';\nimport type { InputOtpBaseProps, InputOtpMode, InputOtpSlotBaseProps } from '../lib/core';\nimport styles from './input-otp.module.scss';\n\ninterface InputOtpContextValue {\n\tvalue: string;\n\tmaxLength: number;\n\tactiveIndex: number;\n\tmode: InputOtpMode;\n\tdisabled: boolean;\n\tregisterSlot: (index: number, element: HTMLInputElement | null) => void;\n\tsetActiveIndex: (index: number) => void;\n\tfocusSlot: (index: number) => void;\n\thandleInputChange: (index: number, inputValue: string) => void;\n\thandleSlotKeyDown: (index: number, event: React.KeyboardEvent<HTMLInputElement>) => void;\n\thandlePaste: (event: React.ClipboardEvent<HTMLInputElement>) => void;\n}\n\nconst InputOtpContext = createContext<InputOtpContextValue | null>(null);\n\nfunction useInputOtpContext(): InputOtpContextValue {\n\tconst context = useContext(InputOtpContext);\n\tif (!context) {\n\t\tthrow new Error('InputOTP sub-components must be used within <InputOTP>.');\n\t}\n\treturn context;\n}\n\nexport interface InputOTPProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'value' | 'defaultValue'>, InputOtpBaseProps {\n\tcontainerClassName?: string;\n}\n\nconst InputOTP = React.forwardRef<HTMLDivElement, InputOTPProps>(\n\t(\n\t\t{ className = '', containerClassName = '', value, defaultValue = '', maxLength = 4, disabled = false, mode = 'alphanumeric', onChange, onComplete, children, ...props },\n\t\tref,\n\t) => {\n\t\tconst [internalValue, setInternalValue] = useState(defaultValue);\n\t\tconst isControlled = value !== undefined;\n\t\tconst currentValue = isControlled ? value : internalValue;\n\t\tconst [activeIndex, setActiveIndexState] = useState(0);\n\t\tconst slotsRef = useRef<(HTMLInputElement | null)[]>([]);\n\n\t\tfunction commit(next: string) {\n\t\t\tconst sanitized = Array.from(next)\n\t\t\t\t.filter((char) => isOtpCharAllowed(char, mode))\n\t\t\t\t.slice(0, maxLength)\n\t\t\t\t.join('');\n\t\t\tif (!isControlled) {\n\t\t\t\tsetInternalValue(sanitized);\n\t\t\t}\n\t\t\tonChange?.(sanitized);\n\t\t\tif (sanitized.length === maxLength) {\n\t\t\t\tonComplete?.(sanitized);\n\t\t\t}\n\t\t\treturn sanitized;\n\t\t}\n\n\t\tfunction registerSlot(index: number, element: HTMLInputElement | null) {\n\t\t\tslotsRef.current[index] = element;\n\t\t}\n\n\t\tconst focusSlot = useCallback((index: number) => {\n\t\t\tconst slot = slotsRef.current[index];\n\t\t\tslot?.focus();\n\t\t\tslot?.select();\n\t\t\tsetActiveIndexState(index);\n\t\t}, []);\n\n\t\tconst setActiveIndex = useCallback((index: number) => {\n\t\t\tsetActiveIndexState(index);\n\t\t}, []);\n\n\t\tfunction handleInputChange(index: number, inputValue: string) {\n\t\t\tconst newChar =\n\t\t\t\tArray.from(inputValue)\n\t\t\t\t\t.filter((char) => isOtpCharAllowed(char, mode))\n\t\t\t\t\t.slice(-1)[0] ?? '';\n\t\t\tconst chars = splitOtpValue(currentValue, maxLength);\n\t\t\tchars[index] = newChar;\n\t\t\tcommit(joinOtpValue(chars, maxLength));\n\t\t\tif (newChar) {\n\t\t\t\tconst next = getNextOtpIndex(index, maxLength);\n\t\t\t\tif (next !== null) {\n\t\t\t\t\tfocusSlot(next);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction handleSlotKeyDown(index: number, event: React.KeyboardEvent<HTMLInputElement>) {\n\t\t\tif (event.key === 'Backspace') {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tconst chars = splitOtpValue(currentValue, maxLength);\n\t\t\t\tif (chars[index]) {\n\t\t\t\t\tchars[index] = '';\n\t\t\t\t\tcommit(joinOtpValue(chars, maxLength));\n\t\t\t\t} else {\n\t\t\t\t\tconst prev = getPrevOtpIndex(index);\n\t\t\t\t\tif (prev !== null) {\n\t\t\t\t\t\tchars[prev] = '';\n\t\t\t\t\t\tcommit(joinOtpValue(chars, maxLength));\n\t\t\t\t\t\tfocusSlot(prev);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (event.key === 'ArrowLeft') {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tconst prev = getPrevOtpIndex(index);\n\t\t\t\tif (prev !== null) {\n\t\t\t\t\tfocusSlot(prev);\n\t\t\t\t}\n\t\t\t} else if (event.key === 'ArrowRight') {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tconst next = getNextOtpIndex(index, maxLength);\n\t\t\t\tif (next !== null) {\n\t\t\t\t\tfocusSlot(next);\n\t\t\t\t}\n\t\t\t} else if (event.key === 'Home') {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tfocusSlot(0);\n\t\t\t} else if (event.key === 'End') {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tfocusSlot(maxLength - 1);\n\t\t\t}\n\t\t}\n\n\t\tfunction handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {\n\t\t\tevent.preventDefault();\n\t\t\tconst text = event.clipboardData.getData('text');\n\t\t\tconst pasted = Array.from(text)\n\t\t\t\t.filter((char) => isOtpCharAllowed(char, mode))\n\t\t\t\t.slice(0, maxLength);\n\t\t\tif (pasted.length === 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst chars = splitOtpValue(currentValue, maxLength);\n\t\t\tfor (let i = 0; i < pasted.length && activeIndex + i < maxLength; i++) {\n\t\t\t\tchars[activeIndex + i] = pasted[i];\n\t\t\t}\n\t\t\tcommit(joinOtpValue(chars, maxLength));\n\t\t\tconst nextIndex = Math.min(activeIndex + pasted.length, maxLength - 1);\n\t\t\tfocusSlot(nextIndex);\n\t\t}\n\n\t\treturn (\n\t\t\t<InputOtpContext.Provider\n\t\t\t\tvalue={{\n\t\t\t\t\tvalue: currentValue,\n\t\t\t\t\tmaxLength,\n\t\t\t\t\tactiveIndex,\n\t\t\t\t\tmode,\n\t\t\t\t\tdisabled,\n\t\t\t\t\tregisterSlot,\n\t\t\t\t\tsetActiveIndex,\n\t\t\t\t\tfocusSlot,\n\t\t\t\t\thandleInputChange,\n\t\t\t\t\thandleSlotKeyDown,\n\t\t\t\t\thandlePaste,\n\t\t\t\t}}>\n\t\t\t\t<div\n\t\t\t\t\tref={ref}\n\t\t\t\t\tdata-slot=\"input-otp\"\n\t\t\t\t\tdata-disabled={disabled}\n\t\t\t\t\tclassName={[styles[inputOtpStyleKeys.root], containerClassName, className].filter(Boolean).join(' ')}\n\t\t\t\t\t{...props}>\n\t\t\t\t\t{children}\n\t\t\t\t</div>\n\t\t\t</InputOtpContext.Provider>\n\t\t);\n\t},\n);\nInputOTP.displayName = 'InputOTP';\n\nexport interface InputOTPGroupProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst InputOTPGroup = React.forwardRef<HTMLDivElement, InputOTPGroupProps>(({ className = '', children, ...props }, ref) => {\n\treturn (\n\t\t<div ref={ref} data-slot=\"input-otp-group\" className={[styles[inputOtpStyleKeys.group], className].filter(Boolean).join(' ')} {...props}>\n\t\t\t{children}\n\t\t</div>\n\t);\n});\nInputOTPGroup.displayName = 'InputOTPGroup';\n\nexport interface InputOTPSlotProps extends React.HTMLAttributes<HTMLDivElement>, InputOtpSlotBaseProps {}\n\nconst InputOTPSlot = React.forwardRef<HTMLDivElement, InputOTPSlotProps>(({ className = '', index, children, ...props }, ref) => {\n\tconst context = useInputOtpContext();\n\tconst char = context.value[index] ?? '';\n\tconst isActive = context.activeIndex === index;\n\n\tfunction setRef(element: HTMLInputElement | null) {\n\t\tcontext.registerSlot(index, element);\n\t}\n\n\treturn (\n\t\t<div ref={ref} data-slot=\"input-otp-slot\" data-active={isActive} className={[styles[inputOtpStyleKeys.slot], className].filter(Boolean).join(' ')} {...props}>\n\t\t\t<input\n\t\t\t\tref={setRef}\n\t\t\t\tvalue={char}\n\t\t\t\tdisabled={context.disabled}\n\t\t\t\tinputMode={context.mode === 'numeric' ? 'numeric' : 'text'}\n\t\t\t\tautoComplete=\"one-time-code\"\n\t\t\t\taria-label={`Character ${index + 1}`}\n\t\t\t\tonFocus={(event) => {\n\t\t\t\t\tcontext.setActiveIndex(index);\n\t\t\t\t\tevent.target.select();\n\t\t\t\t}}\n\t\t\t\tonChange={(event) => context.handleInputChange(index, event.target.value)}\n\t\t\t\tonKeyDown={(event) => context.handleSlotKeyDown(index, event)}\n\t\t\t\tonPaste={context.handlePaste}\n\t\t\t/>\n\t\t\t{children}\n\t\t</div>\n\t);\n});\nInputOTPSlot.displayName = 'InputOTPSlot';\n\nexport interface InputOTPSeparatorProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst InputOTPSeparator = React.forwardRef<HTMLDivElement, InputOTPSeparatorProps>(({ className = '', ...props }, ref) => {\n\treturn (\n\t\t<div ref={ref} data-slot=\"input-otp-separator\" role=\"separator\" className={[styles[inputOtpStyleKeys.separator], className].filter(Boolean).join(' ')} {...props}>\n\t\t\t<svg\n\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\twidth=\"16\"\n\t\t\t\theight=\"16\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\taria-hidden=\"true\">\n\t\t\t\t<path d=\"M5 12h14\" />\n\t\t\t</svg>\n\t\t</div>\n\t);\n});\nInputOTPSeparator.displayName = 'InputOTPSeparator';\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };\nexport default InputOTP;\n"
    },
    {
      "path": "packages/styles/scss/components/input-otp.module.scss",
      "type": "registry:file",
      "target": "components/ui/input-otp/input-otp.module.scss",
      "content": "@use '../../../styles/variables/colors' as *;\n\n// OTP 输入根容器\n.root {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: $spacing-2;\n\n\t&[data-disabled='true'] {\n\t\topacity: 0.5;\n\t}\n}\n\n// OTP 分组\n.group {\n\tdisplay: flex;\n\talign-items: center;\n}\n\n// 单个槽位\n.slot {\n\tposition: relative;\n\tdisplay: flex;\n\twidth: var(--tile-input-otp-size, 2.25rem);\n\theight: var(--tile-input-otp-size, 2.25rem);\n\talign-items: center;\n\tjustify-content: center;\n\tborder-top: 1px solid var(--tile-input-otp-border, #{$input});\n\tborder-right: 1px solid var(--tile-input-otp-border, #{$input});\n\tborder-bottom: 1px solid var(--tile-input-otp-border, #{$input});\n\tbackground-color: var(--tile-input-otp-background, #{$background});\n\tcolor: $foreground;\n\tfont-size: var(--tile-input-otp-font-size, #{$text-sm});\n\tline-height: var(--tile-input-otp-line-height, inherit);\n\tbox-shadow: var(--tile-input-otp-shadow, #{$shadow-sm});\n\ttransition:\n\t\tborder-color $transition-fast,\n\t\tbox-shadow $transition-fast;\n\toutline: none;\n\n\t&:first-child {\n\t\tborder-left: 1px solid var(--tile-input-otp-border, #{$input});\n\t\tborder-radius: var(--tile-input-otp-radius, #{$radius-sm}) 0 0 var(--tile-input-otp-radius, #{$radius-sm});\n\t}\n\n\t&:last-child {\n\t\tborder-radius: 0 var(--tile-input-otp-radius, #{$radius-sm}) var(--tile-input-otp-radius, #{$radius-sm}) 0;\n\t}\n\n\t&[data-active='true'] {\n\t\tz-index: 10;\n\t\tborder-color: var(--tile-input-otp-focus-border, #{$ring});\n\t\tbox-shadow: var(--tile-input-otp-focus-shadow, 0 0 0 3px color-mix(in srgb, #{$ring} 50%, transparent));\n\t}\n\n\tinput {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tborder: none;\n\t\tbackground: transparent;\n\t\tcolor: inherit;\n\t\tfont-size: inherit;\n\t\ttext-align: center;\n\t\toutline: none;\n\n\t\t&:disabled {\n\t\t\tcursor: not-allowed;\n\t\t}\n\t}\n}\n\n// 闪烁光标\n.caret {\n\tposition: absolute;\n\tinset: 0;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tpointer-events: none;\n\n\tspan {\n\t\twidth: 1px;\n\t\theight: $spacing-4;\n\t\tbackground-color: $foreground;\n\t\tanimation: inputOtpCaretBlink 1s steps(1) infinite;\n\t}\n}\n\n@keyframes inputOtpCaretBlink {\n\t0%,\n\t100% {\n\t\topacity: 1;\n\t}\n\n\t50% {\n\t\topacity: 0;\n\t}\n}\n\n// 分隔符\n.separator {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tcolor: $muted-foreground;\n\n\tsvg {\n\t\twidth: $spacing-4;\n\t\theight: $spacing-4;\n\t}\n}\n"
    },
    {
      "path": "packages/react/src/components/input-otp/index.ts",
      "type": "registry:ui",
      "target": "components/ui/input-otp/index.ts",
      "content": "export {\n\tInputOTP,\n\tInputOTPGroup,\n\tInputOTPSlot,\n\tInputOTPSeparator,\n\ttype InputOTPProps,\n\ttype InputOTPGroupProps,\n\ttype InputOTPSlotProps,\n\ttype InputOTPSeparatorProps,\n} from './input-otp';\n"
    }
  ]
}
