{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "opentui/path-input",
  "title": "Path Input",
  "description": "Filesystem path input with tab autocomplete",
  "dependencies": [
    "@opentui/react"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/opentui/use-theme.json"
  ],
  "files": [
    {
      "path": "registry/opentui/ui/path-input.tsx",
      "content": "/* @jsxImportSource @opentui/react */\nimport { readdirSync, statSync } from \"node:fs\";\nimport { join, dirname, basename } from \"node:path\";\n\nimport { useKeyboard } from \"@opentui/react\";\nimport { useState } from \"react\";\n\nimport { useTheme } from \"@/hooks/use-theme\";\n\nexport interface PathInputProps {\n  value?: string;\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}\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    if (inputPath.endsWith(\"/\")) {\n      dir = inputPath;\n      prefix = \"\";\n    } else {\n      dir = dirname(inputPath);\n      prefix = basename(inputPath);\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  onChange,\n  onSubmit,\n  label,\n  placeholder = \"/\",\n  autoFocus: _autoFocus = false,\n  id: _id,\n  width = 40,\n  filter,\n  dirsOnly = false,\n}: PathInputProps) => {\n  const [internalValue, setInternalValue] = useState(\"\");\n  const [completionIndex, setCompletionIndex] = useState(0);\n  const theme = useTheme();\n  const [isFocused] = useState(true);\n\n  const value = controlledValue ?? internalValue;\n  const completions =\n    isFocused && value.length > 0\n      ? getCompletions(value, filter, dirsOnly)\n      : [];\n\n  useKeyboard((key) => {\n    if (!isFocused) {\n      return;\n    }\n    if (key.name === \"return\") {\n      onSubmit?.(value);\n      return;\n    }\n    if (key.name === \"tab\") {\n      if (completions.length > 0) {\n        const selected = completions[completionIndex] ?? completions[0];\n        if (onChange) {\n          onChange(selected);\n        } else {\n          setInternalValue(selected);\n        }\n        setCompletionIndex(0);\n      }\n      return;\n    }\n    if (key.name === \"up\") {\n      setCompletionIndex((c) => Math.max(0, c - 1));\n      return;\n    }\n    if (key.name === \"down\") {\n      setCompletionIndex((c) => Math.min(completions.length - 1, c + 1));\n      return;\n    }\n    if (key.name === \"backspace\" || key.name === \"delete\") {\n      const newVal = value.slice(0, -1);\n      setCompletionIndex(0);\n      if (onChange) {\n        onChange(newVal);\n      } else {\n        setInternalValue(newVal);\n      }\n      return;\n    }\n    if (key.name === \"escape\") {\n      return;\n    }\n    if (key.name.length === 1) {\n      const newVal = value + key.name;\n      setCompletionIndex(0);\n      if (onChange) {\n        onChange(newVal);\n      } else {\n        setInternalValue(newVal);\n      }\n    }\n  });\n\n  const borderColor = isFocused ? theme.colors.focusRing : theme.colors.border;\n\n  return (\n    <box flexDirection=\"column\">\n      {label && (\n        <text>\n          <b>{label}</b>\n        </text>\n      )}\n      <box\n        borderStyle=\"rounded\"\n        borderColor={borderColor}\n        width={width}\n        paddingLeft={1}\n        paddingRight={1}\n      >\n        <text\n          fg={value ? theme.colors.foreground : theme.colors.mutedForeground}\n        >\n          {value || placeholder}\n        </text>\n        {isFocused && <text fg={theme.colors.focusRing}>█</text>}\n      </box>\n      {isFocused && completions.length > 0 && (\n        <box\n          flexDirection=\"column\"\n          borderStyle=\"single\"\n          borderColor={theme.colors.border}\n        >\n          {completions.map((c, idx) => (\n            <box key={c} paddingLeft={1} paddingRight={1}>\n              <text\n                fg={\n                  idx === completionIndex\n                    ? theme.colors.selectionForeground\n                    : theme.colors.foreground\n                }\n                bg={\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 fg=\"#666\">Tab: accept · ↑↓: navigate</text>\n      )}\n    </box>\n  );\n};\n",
      "type": "registry:ui",
      "target": "components/ui/path-input.tsx"
    }
  ],
  "categories": [
    "input"
  ],
  "type": "registry:ui"
}