{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/email-input",
  "title": "Email Input",
  "description": "Email input with validation and domain suggestions",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "https://termcn.dev/r/ink/terminal-style.json",
    "https://termcn.dev/r/ink/terminal-symbols.json",
    "https://termcn.dev/r/ink/terminal-text.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/email-input.tsx",
      "content": "import { Box, Text } from \"ink\";\nimport React, { useState } from \"react\";\n\nimport { useInteraction } from \"@/hooks/use-interaction\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\nimport { resolveBorderStyle } from \"@/lib/terminal-style\";\nimport { resolveTerminalSymbol } from \"@/lib/terminal-symbols\";\nimport {\n  graphemeLength,\n  removeGraphemeBefore,\n} from \"@/lib/terminal-text\";\n\nexport interface EmailInputProps {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  onChange?: (value: string) => void;\n  onSubmit?: (value: string) => void;\n  label?: string;\n  placeholder?: string;\n  autoFocus?: boolean;\n  id?: string;\n  width?: number;\n  suggestions?: string[];\n  isActive?: boolean;\n  disabled?: boolean;\n  readOnly?: boolean;\n  required?: boolean;\n  \"aria-label\"?: string;\n}\n\nconst isValidEmail = (email: string): boolean => {\n  const atIdx = email.indexOf(\"@\");\n  if (atIdx < 1) {\n    return false;\n  }\n  const domain = email.slice(atIdx + 1);\n  return domain.includes(\".\");\n};\n\nconst getBorderColor = (\n  error: string | null,\n  isFocused: boolean,\n  theme: ReturnType<typeof useTheme>\n): string => {\n  if (error) {\n    return theme.colors.error;\n  }\n  if (isFocused) {\n    return theme.colors.focusRing;\n  }\n  return theme.colors.border;\n};\n\nexport const EmailInput = ({\n  value: controlledValue,\n  defaultValue = \"\",\n  onValueChange,\n  onChange,\n  onSubmit,\n  label,\n  placeholder = \"you@example.com\",\n  autoFocus = false,\n  id,\n  width = 40,\n  suggestions = [\"gmail.com\", \"yahoo.com\", \"hotmail.com\", \"outlook.com\"],\n  isActive = true,\n  disabled = false,\n  readOnly = false,\n  required = false,\n  \"aria-label\": ariaLabel,\n}: EmailInputProps) => {\n  const unicode = useUnicode();\n  const cursor = resolveTerminalSymbol(unicode, \"█\", \"|\");\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const [error, setError] = useState<string | null>(null);\n  const theme = useTheme();\n\n  const value = controlledValue ?? internalValue;\n\n  const applyChange = (newVal: string) => {\n    const changeHandler = onValueChange ?? onChange;\n    if (changeHandler) {\n      changeHandler(newVal);\n    } else {\n      setInternalValue(newVal);\n    }\n  };\n\n  const getSuggestion = (val: string): string | null => {\n    const atIdx = val.indexOf(\"@\");\n    if (atIdx === -1) {\n      return null;\n    }\n    const afterAt = val.slice(atIdx + 1);\n    if (afterAt.length === 0) {\n      return null;\n    }\n    const match = suggestions.find(\n      (s) => s.startsWith(afterAt) && s !== afterAt\n    );\n    if (!match) {\n      return null;\n    }\n    return match.slice(afterAt.length);\n  };\n\n  const { isFocused } = useInteraction(\n    (input, key) => {\n      if (key.return) {\n        if (!isValidEmail(value)) {\n          setError(\"Please enter a valid email address\");\n          return;\n        }\n        setError(null);\n        onSubmit?.(value);\n        return;\n      }\n\n      if (key.rightArrow) {\n        const hint = getSuggestion(value);\n        if (hint) {\n          const newVal = value + hint;\n          applyChange(newVal);\n        }\n        return;\n      }\n\n      if (readOnly) {\n        return;\n      }\n\n      if (key.backspace) {\n        setError(null);\n        const newVal = removeGraphemeBefore(value, graphemeLength(value)).value;\n        applyChange(newVal);\n        return;\n      }\n\n      if (key.escape || key.upArrow || key.downArrow) {\n        return;\n      }\n\n      setError(null);\n      if (input && !(key.ctrl || key.meta)) {\n        applyChange(value + input);\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  const borderColor = getBorderColor(error, isFocused, theme);\n\n  const suggestion = getSuggestion(value);\n\n  return (\n    <Box flexDirection=\"column\">\n      {label && <Text bold>{label}</Text>}\n      <Box\n        aria-label={\n          ariaLabel ??\n          `${label ?? \"Email\"}: ${value || \"empty\"}${\n            error ? `. Error: ${error}` : \"\"\n          }`\n        }\n        aria-role=\"textbox\"\n        aria-state={{ disabled, readonly: readOnly, required }}\n        borderStyle={resolveBorderStyle(\"round\", unicode)}\n        borderColor={borderColor}\n        width={width}\n        paddingX={1}\n      >\n        <Text\n          color={value ? theme.colors.foreground : theme.colors.mutedForeground}\n        >\n          {value || placeholder}\n        </Text>\n        {isFocused && suggestion && (\n          <Text aria-hidden color={theme.colors.mutedForeground} dimColor>\n            {suggestion}\n          </Text>\n        )}\n        {isFocused && (\n          <Text aria-hidden color={theme.colors.focusRing}>\n            {cursor}\n          </Text>\n        )}\n      </Box>\n      {error && <Text color={theme.colors.error}>{error}</Text>}\n      {isFocused && suggestion && (\n        <Text aria-hidden color={theme.colors.mutedForeground} dimColor>\n          Right arrow to complete: {value}\n          {suggestion}\n        </Text>\n      )}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "input"
  ],
  "type": "registry:ui"
}