{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/path-input",
  "title": "Path Input",
  "description": "Filesystem path input with tab autocomplete",
  "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/path-input.tsx",
      "content": "import { readdirSync, statSync } from \"node:fs\";\nimport { join, dirname, basename } from \"node:path\";\n\nimport { 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 PathInputProps {\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  filter?: string;\n  dirsOnly?: boolean;\n  isActive?: boolean;\n  disabled?: boolean;\n  readOnly?: boolean;\n  required?: boolean;\n  \"aria-label\"?: string;\n}\n\nconst getCompletions = (\n  inputPath: string,\n  filter?: string,\n  dirsOnly?: boolean\n): string[] => {\n  try {\n    let dir: string;\n    let prefix: string;\n\n    if (inputPath.endsWith(\"/\")) {\n      dir = inputPath;\n      prefix = \"\";\n    } else {\n      dir = dirname(inputPath);\n      prefix = basename(inputPath);\n    }\n\n    const entries = readdirSync(dir || \"/\");\n    return entries\n      .filter((e) => e.startsWith(prefix))\n      .filter((e) => {\n        try {\n          const fullPath = join(dir, e);\n          const stat = statSync(fullPath);\n          if (dirsOnly && !stat.isDirectory()) {\n            return false;\n          }\n          if (filter && !stat.isDirectory() && !e.endsWith(filter)) {\n            return false;\n          }\n          return true;\n        } catch {\n          return false;\n        }\n      })\n      .map((e) => {\n        try {\n          const fullPath = join(dir, e);\n          const stat = statSync(fullPath);\n          return stat.isDirectory() ? `${join(dir, e)}/` : join(dir, e);\n        } catch {\n          return join(dir, e);\n        }\n      })\n      .slice(0, 5);\n  } catch {\n    return [];\n  }\n};\n\nexport const PathInput = ({\n  value: controlledValue,\n  defaultValue = \"\",\n  onValueChange,\n  onChange,\n  onSubmit,\n  label,\n  placeholder = \"/\",\n  autoFocus = false,\n  id,\n  width = 40,\n  filter,\n  dirsOnly = false,\n  isActive = true,\n  disabled = false,\n  readOnly = false,\n  required = false,\n  \"aria-label\": ariaLabel,\n}: PathInputProps) => {\n  const unicode = useUnicode();\n  const cursor = resolveTerminalSymbol(unicode, \"█\", \"|\");\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const [completionIndex, setCompletionIndex] = useState(0);\n  const theme = useTheme();\n\n  const value = controlledValue ?? internalValue;\n  const applyChange = (nextValue: string) => {\n    const changeHandler = onValueChange ?? onChange;\n    if (changeHandler) {\n      changeHandler(nextValue);\n    } else {\n      setInternalValue(nextValue);\n    }\n  };\n\n  const { isFocused } = useInteraction(\n    (input, key) => {\n      const completionOptions = getCompletions(value, filter, dirsOnly);\n\n      if (key.return) {\n        onSubmit?.(value);\n        return;\n      }\n\n      if (key.rightArrow) {\n        if (completionOptions.length > 0) {\n          const selected =\n            completionOptions[completionIndex] ?? completionOptions[0];\n          applyChange(selected);\n          setCompletionIndex(0);\n        }\n        return;\n      }\n\n      if (key.upArrow) {\n        setCompletionIndex((c) => Math.max(0, c - 1));\n        return;\n      }\n\n      if (key.downArrow) {\n        setCompletionIndex((c) =>\n          Math.min(completionOptions.length - 1, c + 1)\n        );\n        return;\n      }\n\n      if (readOnly) {\n        return;\n      }\n\n      if (key.backspace) {\n        const newVal = removeGraphemeBefore(value, graphemeLength(value)).value;\n        setCompletionIndex(0);\n        applyChange(newVal);\n        return;\n      }\n\n      if (key.escape) {\n        return;\n      }\n\n      if (input && !(key.ctrl || key.meta)) {\n        setCompletionIndex(0);\n        applyChange(value + input);\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  const completions =\n    isFocused && value.length > 0\n      ? getCompletions(value, filter, dirsOnly)\n      : [];\n\n  const borderColor = isFocused ? theme.colors.focusRing : theme.colors.border;\n\n  return (\n    <Box flexDirection=\"column\">\n      {label && <Text bold>{label}</Text>}\n      <Box\n        aria-label={ariaLabel ?? `${label ?? \"Path\"}: ${value || \"empty\"}`}\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          aria-hidden\n          color={value ? theme.colors.foreground : theme.colors.mutedForeground}\n        >\n          {value || placeholder}\n        </Text>\n        {isFocused && (\n          <Text aria-hidden color={theme.colors.focusRing}>\n            {cursor}\n          </Text>\n        )}\n      </Box>\n      {isFocused && completions.length > 0 && (\n        <Box\n          aria-role=\"listbox\"\n          flexDirection=\"column\"\n          borderStyle={resolveBorderStyle(\"single\", unicode)}\n          borderColor={theme.colors.border}\n          width={width}\n        >\n          {completions.map((c, idx) => (\n            <Box\n              key={c}\n              aria-label={c}\n              aria-role=\"option\"\n              aria-state={{ selected: idx === completionIndex }}\n              paddingX={1}\n            >\n              <Text\n                aria-hidden\n                color={\n                  idx === completionIndex\n                    ? theme.colors.selectionForeground\n                    : theme.colors.foreground\n                }\n                backgroundColor={\n                  idx === completionIndex ? theme.colors.selection : undefined\n                }\n              >\n                {c}\n              </Text>\n            </Box>\n          ))}\n        </Box>\n      )}\n      {isFocused && completions.length > 0 && (\n        <Text aria-hidden color={theme.colors.mutedForeground} dimColor>\n          {resolveTerminalSymbol(\n            unicode,\n            \"Right arrow: accept · ↑↓: navigate\",\n            \"Right arrow: accept | up/down: navigate\"\n          )}\n        </Text>\n      )}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "input"
  ],
  "type": "registry:ui"
}