{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "opentui/text-input",
  "title": "Text Input",
  "description": "Single-line text input with placeholder, validation, mask",
  "dependencies": [
    "@opentui/react"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/opentui/use-theme.json"
  ],
  "files": [
    {
      "path": "registry/opentui/ui/text-input.tsx",
      "content": "/* @jsxImportSource @opentui/react */\nimport { useKeyboard } from \"@opentui/react\";\nimport { useEffect, useState } from \"react\";\n\nimport { useTheme } from \"@/hooks/use-theme\";\nimport type { BorderStyle } from \"@/components/ui/types\";\n\nexport interface TextInputProps {\n  value?: string;\n  onChange?: (value: string) => void;\n  onSubmit?: (value: string) => void;\n  placeholder?: string;\n  mask?: string;\n  showCursor?: boolean;\n  highlightPastedText?: boolean;\n  validate?: (value: string) => string | null;\n  width?: number;\n  label?: string;\n  autoFocus?: boolean;\n  id?: string;\n  bordered?: boolean;\n  borderStyle?: BorderStyle;\n  paddingX?: number;\n  cursor?: string;\n}\n\nexport const TextInput = ({\n  value: controlledValue,\n  onChange,\n  onSubmit,\n  placeholder = \"\",\n  mask,\n  showCursor = true,\n  highlightPastedText = false,\n  validate,\n  width = 40,\n  label,\n  autoFocus: _autoFocus = false,\n  id: _id,\n  bordered = true,\n  borderStyle = \"rounded\",\n  paddingX = 1,\n  cursor = \"█\",\n}: TextInputProps) => {\n  const [internalValue, setInternalValue] = useState(\"\");\n  const [cursorOffset, setCursorOffset] = useState(0);\n  const [cursorWidth, setCursorWidth] = useState(0);\n  const [error, setError] = useState<string | null>(null);\n  const theme = useTheme();\n  const [isFocused] = useState(true);\n\n  const value = controlledValue ?? internalValue;\n\n  useEffect(() => {\n    if (cursorOffset > value.length) {\n      setCursorOffset(value.length);\n    }\n  }, [value, cursorOffset]);\n\n  const setValue = (next: string) => {\n    if (onChange) {\n      onChange(next);\n    } else {\n      setInternalValue(next);\n    }\n  };\n\n  useKeyboard((key) => {\n    if (!isFocused) {\n      return;\n    }\n    if (\n      key.name === \"up\" ||\n      key.name === \"down\" ||\n      (key.ctrl && key.name === \"c\") ||\n      key.name === \"tab\"\n    ) {\n      return;\n    }\n    if (key.name === \"return\") {\n      const err = validate ? validate(value) : null;\n      if (err) {\n        setError(err);\n        return;\n      }\n      setError(null);\n      onSubmit?.(value);\n      return;\n    }\n    if (key.name === \"escape\") {\n      return;\n    }\n    let nextOffset = cursorOffset;\n    let nextValue = value;\n    let nextCursorWidth = 0;\n    if (key.name === \"left\") {\n      if (showCursor) {\n        nextOffset = Math.max(0, nextOffset - 1);\n      }\n    } else if (key.name === \"right\") {\n      if (showCursor) {\n        nextOffset = Math.min(value.length, nextOffset + 1);\n      }\n    } else if (key.name === \"backspace\" || key.name === \"delete\") {\n      if (cursorOffset > 0) {\n        nextValue =\n          value.slice(0, cursorOffset - 1) + value.slice(cursorOffset);\n        nextOffset = cursorOffset - 1;\n      }\n    } else if (key.name.length === 1) {\n      nextValue =\n        value.slice(0, cursorOffset) + key.name + value.slice(cursorOffset);\n      nextOffset = cursorOffset + key.name.length;\n      if (key.name.length > 1) {\n        nextCursorWidth = key.name.length;\n      }\n    }\n    setCursorOffset(nextOffset);\n    setCursorWidth(nextCursorWidth);\n    if (nextValue !== value) {\n      setValue(nextValue);\n    }\n  });\n\n  const displayValue = mask ? mask.repeat(value.length) : value;\n\n  let borderColor: string;\n  if (error) {\n    borderColor = theme.colors.error;\n  } else if (isFocused) {\n    borderColor = theme.colors.focusRing;\n  } else {\n    borderColor = theme.colors.border;\n  }\n\n  const pasteWidth = highlightPastedText ? cursorWidth : 0;\n\n  const renderValue = () => {\n    if (!value && placeholder) {\n      if (showCursor && isFocused) {\n        return (\n          <>\n            <text fg={theme.colors.mutedForeground} reverse={true}>\n              {placeholder[0] ?? \" \"}\n            </text>\n            <text fg={theme.colors.mutedForeground}>\n              {placeholder.slice(1)}\n            </text>\n          </>\n        );\n      }\n      return <text fg={theme.colors.mutedForeground}>{placeholder}</text>;\n    }\n\n    if (!showCursor || !isFocused) {\n      return <text fg={theme.colors.foreground}>{displayValue}</text>;\n    }\n\n    const before = displayValue.slice(0, cursorOffset - pasteWidth);\n    const highlighted = displayValue.slice(\n      cursorOffset - pasteWidth,\n      cursorOffset\n    );\n    const cursorChar =\n      cursorOffset < displayValue.length ? displayValue[cursorOffset] : cursor;\n    const after = displayValue.slice(cursorOffset + 1);\n\n    return (\n      <>\n        {before && <text fg={theme.colors.foreground}>{before}</text>}\n        {highlighted && (\n          <text fg={theme.colors.foreground} reverse={true}>\n            {highlighted}\n          </text>\n        )}\n        <text reverse={true} fg={theme.colors.focusRing}>\n          {cursorChar}\n        </text>\n        {after && <text fg={theme.colors.foreground}>{after}</text>}\n      </>\n    );\n  };\n  const boxProps = bordered\n    ? {\n        borderColor,\n        borderStyle,\n        paddingLeft: paddingX,\n        paddingRight: paddingX,\n        width,\n      }\n    : { paddingLeft: paddingX, paddingRight: paddingX, width };\n\n  return (\n    <box flexDirection=\"column\">\n      {label && (\n        <text>\n          <b>{label}</b>\n        </text>\n      )}\n      <box {...boxProps}>{renderValue()}</box>\n      {error && <text fg={theme.colors.error}>{error}</text>}\n    </box>\n  );\n};\n",
      "type": "registry:ui",
      "target": "components/ui/text-input.tsx"
    }
  ],
  "categories": [
    "input"
  ],
  "type": "registry:ui"
}