[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81575":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":17,"stars30d":17,"stars90d":16,"forks30d":16,"starsTrendScore":18,"compositeScore":19,"rankGlobal":10,"rankLanguage":10,"license":20,"archived":21,"fork":21,"defaultBranch":22,"hasWiki":23,"hasPages":21,"topics":24,"createdAt":10,"pushedAt":10,"updatedAt":30,"readmeContent":31,"aiSummary":32,"trendingCount":16,"starSnapshotCount":16,"syncStatus":17,"lastSyncTime":33,"discoverSource":34},81575,"react-ink-textarea","omranjamal\u002Freact-ink-textarea","omranjamal","A full-featured CLI textarea component for React Ink — Implement beautiful multi-line text input on the CLI","",null,"TypeScript",26,1,24,7,0,2,6,46.1,"MIT License",false,"main",true,[25,26,27,28,29],"cli","ink","react","reactjs","reactjs-components","2026-06-12 04:01:34","# react-ink-textarea\n> A multiline textarea component for [Ink](https:\u002F\u002Fgithub.com\u002Fvadimdemedes\u002Fink)\n\n[\u003Cimg alt=\"GitHub Issues or Pull Requests\" src=\"https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Fissues\u002Fomranjamal\u002Freact-ink-textarea\">](https:\u002F\u002Fgithub.com\u002Fomranjamal\u002Freact-ink-textarea\u002Fissues) [\u003Cimg alt=\"NPM Downloads\" src=\"https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fdw\u002Freact-ink-textarea\">](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-ink-textarea) [\u003Cimg alt=\"NPM Version\" src=\"https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002Freact-ink-textarea\">](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-ink-textarea) [\u003Cimg alt=\"NPM License\" src=\"https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fl\u002Freact-ink-textarea\">](https:\u002F\u002Fgithub.com\u002Fomranjamal\u002Freact-ink-textarea\u002Fblob\u002Fmain\u002FLICENSE) [\u003Cimg alt=\"GitHub forks\" src=\"https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Fforks\u002Fomranjamal\u002Freact-ink-textarea?style=flat\">](https:\u002F\u002Fgithub.com\u002Fomranjamal\u002Freact-ink-textarea\u002Fnetwork\u002Fmembers)\n\n\n\n\nBuild rich CLI forms with a full-featured textarea that supports multi-line editing, cursor navigation, undo, and customizable line prefixes.\n\n\u003Cimg width=\"580\" alt=\"react-ink-textarea demo\" src=\"https:\u002F\u002Fgithub.com\u002Fuser-attachments\u002Fassets\u002Ffbd7325e-4b16-4fde-96f6-e02a1b839cb9\" \u002F>\n\n\n## Contents\n\n- [Features](#features)\n- [Install](#install)\n- [Usage](#usage)\n  - [1. Basic](#1-basic)\n  - [2. Controlled mode with cursor labels](#2-controlled-mode-with-cursor-labels)\n  - [3. Line numbers and active-line highlight](#3-line-numbers-and-active-line-highlight)\n  - [4. Inline syntax highlighting](#4-inline-syntax-highlighting)\n  - [5. Multi-field form with focus chaining](#5-multi-field-form-with-focus-chaining)\n  - [6. Slash-command picker (arrow + tab handoff)](#6-slash-command-picker-arrow--tab-handoff)\n  - [7. Code-editor preset](#7-code-editor-preset)\n- [Props](#props)\n- [Imperative API (ref)](#imperative-api-ref)\n- [Keybindings](#keybindings)\n  - [Keybinding Toggles](#keybinding-toggles)\n- [Caveats & limitations](#caveats--limitations)\n- [Development](#development)\n- [License](#license)\n\n## Features\n\n- 🎨 Polished feel — blinking cursor that pauses while typing, active-line highlight, multi-line placeholder, optional whitespace glyphs.\n- 🪪 Custom gutter via `linePrefix` render-prop, plus a drop-in `\u003CLineNumberPrefix \u002F>`.\n- 🌈 Regex (or function) labels with per-label styles; cursor reports the label under it.\n- ⌨️ Readline keybindings, configurable per chord. `Tab` is a callback. Grouped undo and bracketed paste.\n- 🌐 Unicode-correct: grapheme cursor, visual-width wrapping, real tab expansion, CRLF normalized.\n- 📐 Built-in viewport virtualization; auto-scroll; resize-aware.\n- 🧭 Boundary callbacks (`onFirstLineUp`, `onLastLineDown`, `onFirstCharacterLeft`, `onLastCharacterRight`) for parent-owned focus chaining.\n- ⚛️ Controlled, uncontrolled, or mixed. Imperative `ref.insert(text)` for autocomplete pickers.\n- 🧷 Strict TypeScript, tree-shakable.\n- 🧪 Works with [`ink-testing-library`](https:\u002F\u002Fgithub.com\u002Fvadimdemedes\u002Fink-testing-library); 250+ tests in-repo.\n\n## Install\n\nInstall [react-ink-textarea](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-ink-textarea) via your JS package manager of choice.\n\n```bash\nnpm install react-ink-textarea\n\n# or\npnpm add react-ink-textarea\n```\n\n## Usage\n\n### 1. Basic\n\nUncontrolled mode. Submit on Enter, freeze on submit.\n\n```tsx\nimport { render } from \"ink\";\nimport { useState } from \"react\";\nimport { TextArea } from \"react-ink-textarea\";\n\nconst App = () => {\n  const [submitted, setSubmitted] = useState(\"\");\n  const [focus, setFocus] = useState(true);\n\n  return (\n    \u003CTextArea\n      focus={focus}\n      placeholder=\"Type your message here...\"\n      onSubmit={(value) => {\n        setSubmitted(value);\n        setFocus(false);\n      }}\n    \u002F>\n  );\n};\n\nrender(\u003CApp \u002F>);\n```\n\n### 2. Controlled mode with cursor labels\n\nOwn `value` and `cursorPosition` externally. `onCursorChange` reports the label and chunk index under the cursor — drive a status line, hover-card, or autocomplete from it.\n\n```tsx\nimport { Box, Text } from \"ink\";\nimport { useState } from \"react\";\nimport { TextArea, type TLabels } from \"react-ink-textarea\";\n\nconst labels: TLabels = [{ pattern: \u002F#\\w+\u002Fg, label: \"tag\" }];\nconst styles = { tag: { color: \"magenta\" } };\n\nconst Editor = () => {\n  const [value, setValue] = useState(\"hello #world\");\n  const [cursor, setCursor] = useState\u003C[number, number]>([0, 0]);\n  const [info, setInfo] = useState(\"text\");\n\n  return (\n    \u003CBox flexDirection=\"column\">\n      \u003CTextArea\n        focus\n        value={value}\n        cursorPosition={cursor}\n        labels={labels}\n        styles={styles}\n        onChange={setValue}\n        \u002F\u002F (pos, labelType, chunkIndex) — labelType is \"text\" outside any match\n        onCursorChange={(pos, type, idx) => {\n          setCursor(pos);\n          setInfo(type === \"text\" ? \"text\" : `${type}#${idx}`);\n        }}\n        onSubmit={() => {}}\n      \u002F>\n      \u003CText dimColor>under cursor: {info}\u003C\u002FText>\n    \u003C\u002FBox>\n  );\n};\n```\n\n### 3. Line numbers and active-line highlight\n\nDrop-in: pass the bundled `LineNumberPrefix` straight to `linePrefix`.\n\n```tsx\nimport { TextArea, LineNumberPrefix } from \"react-ink-textarea\";\n\n\u003CTextArea\n  focus\n  highlightActiveLine\n  activeLineColor=\"#222\"\n  initialLineCount={6}\n  onSubmit={() => {}}\n  linePrefix={LineNumberPrefix}\n\u002F>;\n```\n\nCustom: `linePrefix` is a render prop. Handle `isContinuationLine` and `isVirtualLine` to draw clean gutters when wrapping or padding. Compose with the bundled `LineNumber` for the digits.\n\n```tsx\nimport { Text } from \"ink\";\nimport { TextArea, LineNumber } from \"react-ink-textarea\";\n\n\u003CTextArea\n  focus\n  highlightActiveLine\n  initialLineCount={6}\n  onSubmit={() => {}}\n  linePrefix={({\n    lineNumber,\n    totalLines,\n    isActiveLine,\n    isContinuationLine,\n    isVirtualLine,\n  }) =>\n    isContinuationLine ? (\n      \u003CText color=\"gray\">  ↳ \u003C\u002FText>\n    ) : (\n      \u003CText>\n        \u003CLineNumber\n          lineNumber={lineNumber}\n          totalLines={totalLines}\n          isActive={isActiveLine}\n          padChar=\" \"\n          activeColor=\"cyan\"\n        \u002F>\n        \u003CText color={isVirtualLine ? \"gray\" : \"white\"}> │ \u003C\u002FText>\n      \u003C\u002FText>\n    )\n  }\n\u002F>;\n```\n\n### 4. Inline syntax highlighting\n\n`labels` mixes regex rules with function-form rules for allowlists. First rule wins on overlap. `styles.text` and `styles.invisibleCharacter` are reserved keys; everything else maps to a label name.\n\n```tsx\nimport { useMemo } from \"react\";\nimport { TextArea, type TLabels } from \"react-ink-textarea\";\n\nconst KNOWN_USERS = new Set([\"alice\", \"bob\", \"carol\"]);\n\nconst Editor = () => {\n  const labels = useMemo\u003CTLabels>(\n    () => [\n      { pattern: \u002Fhttps?:\\\u002F\\\u002F\\S+\u002Fg, label: \"url\" },\n      { pattern: \u002F#\\w+\u002Fg, label: \"tag\" },\n      \u002F\u002F function form: leave unknown @handles unstyled by returning undefined\n      {\n        pattern: \u002F@(\\w+)\u002Fg,\n        label: (m) => (KNOWN_USERS.has(m[1]) ? \"mention\" : undefined),\n      },\n    ],\n    [],\n  );\n\n  return (\n    \u003CTextArea\n      focus\n      onSubmit={() => {}}\n      showInvisibles={{ space: false, tab: true, newline: false }}\n      labels={labels}\n      styles={{\n        text: { color: \"white\" },\n        invisibleCharacter: { color: \"gray\", dim: true },\n        url: { color: \"blue\", underline: true },\n        tag: { color: \"magenta\" },\n        mention: { color: \"green\", bold: true },\n      }}\n    \u002F>\n  );\n};\n```\n\n### 5. Multi-field form with focus chaining\n\nBoundary callbacks let you escape the textarea cleanly: ↑ on the first row jumps to the field above, ↓ past the last line jumps below, and ←\u002F→ at the absolute ends do the same horizontally.\n\n```tsx\nimport { Box, useFocusManager, useFocus } from \"ink\";\nimport { useState } from \"react\";\nimport { TextArea } from \"react-ink-textarea\";\nimport TextInput from \"ink-text-input\";\n\nconst Form = () => {\n  const { focusNext, focusPrevious } = useFocusManager();\n  const subject = useFocus({ id: \"subject\" });\n  const body = useFocus({ id: \"body\" });\n  const tags = useFocus({ id: \"tags\" });\n  const [s, setS] = useState(\"\");\n  const [b, setB] = useState(\"\");\n  const [t, setT] = useState(\"\");\n\n  return (\n    \u003CBox flexDirection=\"column\" gap={1}>\n      \u003CTextInput value={s} onChange={setS} focus={subject.isFocused} \u002F>\n      \u003CTextArea\n        focus={body.isFocused}\n        value={b}\n        onChange={setB}\n        onSubmit={() => {}}\n        onFirstLineUp={focusPrevious}\n        onLastLineDown={focusNext}\n        onFirstCharacterLeft={focusPrevious}\n        onLastCharacterRight={focusNext}\n      \u002F>\n      \u003CTextInput value={t} onChange={setT} focus={tags.isFocused} \u002F>\n    \u003C\u002FBox>\n  );\n};\n```\n\n### 6. Slash-command picker (arrow + tab handoff)\n\nWhen a menu opens, suspend cursor navigation with `disableArrowNavigation` and disable `Enter` so the picker — not the textarea — handles them. Re-enable on close.\n\n```tsx\nimport { Box, Text } from \"ink\";\nimport { useState } from \"react\";\nimport { TextArea } from \"react-ink-textarea\";\n\nconst COMMANDS = [\"\u002Fhelp\", \"\u002Fquit\", \"\u002Ftrain\"];\n\nconst Composer = () => {\n  const [value, setValue] = useState(\"\");\n  const [sel, setSel] = useState(0);\n  const open = value.startsWith(\"\u002F\");\n\n  return (\n    \u003CBox flexDirection=\"column\">\n      \u003CTextArea\n        focus\n        value={value}\n        onChange={setValue}\n        onSubmit={(v) => {\n          if (!open) console.log(v);\n        }}\n        \u002F\u002F While the picker is open, give it the arrows + Enter; typing still flows.\n        disableArrowNavigation={open}\n        keybindings={open ? { Enter: false } : undefined}\n        onTab={(shift) =>\n          setSel((i) => (i + (shift ? -1 : 1) + COMMANDS.length) % COMMANDS.length)\n        }\n      \u002F>\n      {open && (\n        \u003CBox flexDirection=\"column\">\n          {COMMANDS.map((c, i) => (\n            \u003CText key={c} color={i === sel ? \"cyan\" : undefined}>\n              {i === sel ? \"▸ \" : \"  \"}\n              {c}\n            \u003C\u002FText>\n          ))}\n        \u003C\u002FBox>\n      )}\n    \u003C\u002FBox>\n  );\n};\n```\n\n### 7. Code-editor preset\n\nOverride the viewport explicitly, expand tabs, lock down ergonomic chords, and surface the measured content width to a status bar.\n\n```tsx\nimport { Box, Text } from \"ink\";\nimport { useState } from \"react\";\nimport { TextArea } from \"react-ink-textarea\";\n\nconst CodeEditor = () => {\n  const [width, setWidth] = useState(0);\n\n  return (\n    \u003CBox flexDirection=\"column\">\n      \u003CTextArea\n        focus\n        onSubmit={() => {}}\n        viewportLines={20}\n        tabWidth={2}\n        autoNewLineLimit={1}\n        showInvisibles\n        keybindings={{\n          \u002F\u002F Single-undo-stack ergonomics: defer undo to the host (e.g. file-level)\n          \"Ctrl+Z\": false,\n          \u002F\u002F Keep Shift+Enter free for the parent's \"submit-with-newline\" gesture\n          \"Shift+Enter\": false,\n        }}\n        onDimensions={setWidth}\n      \u002F>\n      \u003CText dimColor>width: {width} cols\u003C\u002FText>\n    \u003C\u002FBox>\n  );\n};\n```\n\n## Props\n\n| Prop                    | Type                                                                                        | Description                                                                                                                               |\n| ----------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| `focus`                 | `boolean`                                                                                   | Whether the textarea is focused and receiving keyboard input.                                                                             |\n| `onSubmit`              | `(value: string) => void`                                                                   | Called when the user presses **Enter**. Receives the full text.                                                                           |\n| `placeholder`           | `string`                                                                                    | Placeholder text shown when the textarea is empty.                                                                                        |\n| `linePrefix`            | `ReactNode \\| (props: TLinePrefixProps) => ReactNode`                                       | Optional prefix rendered before each line. The function form receives `{ lineNumber, totalLines, isActiveLine, isVirtualLine, isContinuationLine, continuationIndex }`. Use for line numbers, gutters, borders, etc. |\n| `highlightActiveLine`   | `boolean`                                                                                   | When `true`, the active line is highlighted with a subtle background color. Defaults to `false`.                                          |\n| `activeLineColor`       | `string`                                                                                    | Background color for the active line highlight. Defaults to no color.                                                                     |\n| `cursorInterval`        | `number`                                                                                    | Cursor blink interval in milliseconds. Defaults to `500`.                                                                                 |\n| `typingPause`           | `number`                                                                                    | Milliseconds to wait after typing before resuming cursor blink. Defaults to `450`.                                                        |\n| `maxUndo`               | `number`                                                                                    | Maximum number of undo steps to retain. Defaults to `128`.                                                                                |\n| `undoGroupDelay`        | `number`                                                                                    | Milliseconds to group consecutive edits into a single undo step. Defaults to `750`.                                                       |\n| `autoNewLineLimit`      | `number`                                                                                    | Maximum number of empty lines allowed after the last line with content. Only applies to Down arrow navigation. Defaults to `3`.           |\n| `disableArrowNavigation` | `boolean`                                                                                  | When `true`, disables cursor movement via arrow keys (and word\u002Fline jumps). Useful for implementing suggestion pickers. Defaults to `false`.                    |\n| `keybindings`           | `Partial\u003CRecord\u003CTKeybinding, boolean>>`                                                     | Per-chord enable\u002Fdisable map. Merged over defaults (all `true`). Set a chord to `false` to swallow it. `disableArrowNavigation: true` additionally forces all nav chords off. See **Keybinding Toggles** below.        |\n| `initialLineCount`      | `number`                                                                                    | Number of lines to display initially. The textarea will maintain at least this many lines. Defaults to `2`.                               |\n| `viewportLines`         | `number`                                                                                    | Maximum number of visual rows rendered at once. The textarea virtualizes rendering and auto-scrolls to keep the cursor visible. Defaults to `floor(stdout.rows * 0.5)` so blink re-renders don't scroll-jank tall buffers when the frame exceeds the terminal viewport. Pass an explicit number to override; `Infinity` renders every row. |\n| `tabWidth`              | `number`                                                                                    | Visual width of `\\t` characters in cells. Tabs render as `tabWidth` spaces (or `→` + spaces with `showInvisibles.tab`). The stored value keeps `\\t`. Defaults to `4`. |\n| `value`                 | `string`                                                                                    | **Controlled mode**: The current value of the textarea. When provided, component operates in controlled mode.                             |\n| `cursorPosition`        | `[line: number, column: number]`                                                            | **Controlled mode**: The current cursor position as a `[line, column]` tuple. Use with `value` for full control.                          |\n| `onChange`              | `(value: string) => void`                                                                   | **Controlled mode**: Called when the value changes.                                                                                       |\n| `onCursorChange`        | `(position: [line, column], type: string, chunkIndex: number) => void`                      | **Controlled mode**: Called when the cursor moves. `type` is the label at the cursor (`\"text\"` if no label matches); `chunkIndex` is the zero-based index of the labeled segment the cursor is in. |\n| `onFirstLineUp`         | `() => void`                                                                                | Called when Up arrow is pressed on the first visual row. Useful for moving focus out of the textarea.                                     |\n| `onLastLineDown`        | `() => void`                                                                                | Called when Down arrow is pressed on the last line and trailing-empty-line limit is reached. Useful for moving focus out.                 |\n| `onFirstCharacterLeft`  | `() => void`                                                                                | Called when Left arrow is pressed at the very start of the value (`cursor === 0`). Useful for moving focus to a previous field.            |\n| `onLastCharacterRight`  | `() => void`                                                                                | Called when Right arrow is pressed at the very end of the value (`cursor === value.length`). Useful for moving focus to a next field.       |\n| `onTab`                 | `(shift: boolean) => void`                                                                  | Called when Tab is pressed. `shift` is `true` for Shift+Tab. Without this prop, Tab is silently swallowed (no value mutation).            |\n| `onDimensions`          | `(width: number) => void`                                                                   | Called with the measured content width whenever it changes.                                                                               |\n| `showInvisibles`        | `boolean \\| { space?: boolean; tab?: boolean; newline?: boolean }`                          | Render whitespace glyphs (`·` for space, `→` for tab, `↵` for newline). Defaults to `false`.                                              |\n| `styles`                | `{ text?, invisibleCharacter?, [labelName]? }` of `TStyleProps`                             | Style overrides for the default text run, invisible glyphs, and any user-defined labels. `color` and `bgColor` accept any value Ink's `\u003CText>` accepts — see the [Ink color reference](https:\u002F\u002Fgithub.com\u002Fvadimdemedes\u002Fink#color). |\n| `labels`                | `readonly { pattern: RegExp; label: string \\| ((match: RegExpMatchArray) => string \\| undefined) }[]` | Array of label rules. Each rule's `pattern` is matched against the value; matches receive the rule's `label`. Use a function form to allowlist matches — return `undefined` to leave a match unlabeled. First rule wins on overlap. |\n\n## Imperative API (ref)\n\nPass a `ref` of type `TextAreaHandle` to insert text programmatically — typically from an autocomplete picker. Insertion happens at the current cursor and advances it. Works in both controlled and uncontrolled modes.\n\n```tsx\nimport { useRef } from \"react\";\nimport { TextArea, type TextAreaHandle } from \"react-ink-textarea\";\n\nconst Composer = () => {\n  const ref = useRef\u003CTextAreaHandle>(null);\n  return (\n    \u003CTextArea\n      ref={ref}\n      focus\n      onSubmit={() => {}}\n      onTab={() => ref.current?.insert(\"\u002Fhelp \")}\n    \u002F>\n  );\n};\n```\n\n| Method                  | Description                                                                                  |\n| ----------------------- | -------------------------------------------------------------------------------------------- |\n| `insert(text: string)`  | Insert `text` at the current cursor and advance it past the inserted text. Empty string is a no-op. |\n\n## Keybindings\n\n| Key             | Action                         |\n| --------------- | ------------------------------ |\n| `Ctrl+J`        | Insert newline                 |\n| `Ctrl+Enter`    | Insert newline                 |\n| `Shift+Enter`   | Insert newline                 |\n| `Alt+Enter`     | Insert newline (Option+Enter)  |\n| `Enter`         | Submit                         |\n| `↑` \u002F `↓`       | Move cursor between lines      |\n| `←` \u002F `→`       | Move cursor left \u002F right       |\n| `Opt+←`         | Jump to previous word          |\n| `Opt+→`         | Jump to next word              |\n| `Ctrl+A`        | Start of current line          |\n| `Ctrl+E`        | End of current line            |\n| `Ctrl+W`        | Delete word before cursor      |\n| `Ctrl+U`        | Delete to start of line. At column 0, joins with previous line (matches Cmd+Backspace mapping in iTerm2\u002Fghostty). |\n| `Ctrl+K`        | Delete to end of line          |\n| `Backspace`     | Delete character before cursor |\n| `Delete`        | Delete character before cursor (same as `Backspace`) |\n| `Opt+Backspace` | Delete word before cursor      |\n| `Ctrl+Z`        | Undo (up to 128 steps)         |\n\n> On macOS, `Alt` chords are pressed via the **Option** (`⌥`) key.\n\n### Keybinding Toggles\n\nPass a `keybindings` map to disable individual chords. Keys are the chord strings themselves; values are `true` (enabled) or `false` (disabled). Anything you don't list defaults to enabled.\n\n```tsx\n\u003CTextArea\n  focus\n  onSubmit={onSubmit}\n  keybindings={{\n    \"Ctrl+Z\": false,      \u002F\u002F disable undo\n    \"Shift+Enter\": false, \u002F\u002F disable Shift+Enter newline (other newline chords still work)\n    \"Alt+B\": false,       \u002F\u002F disable previous-word jump\n  }}\n\u002F>\n```\n\nThe full chord catalog (every key is a `TKeybinding`):\n\n| Chord            | Action                            |\n| ---------------- | --------------------------------- |\n| `Enter`          | Submit                            |\n| `Ctrl+J`         | Insert newline                    |\n| `Ctrl+Enter`     | Insert newline                    |\n| `Shift+Enter`    | Insert newline                    |\n| `Alt+Enter`      | Insert newline                    |\n| `Up`             | Cursor up                         |\n| `Down`           | Cursor down                       |\n| `Left`           | Cursor left                       |\n| `Right`          | Cursor right                      |\n| `Alt+B`          | Previous word                     |\n| `Alt+F`          | Next word                         |\n| `Ctrl+A`         | Start of line                     |\n| `Ctrl+E`         | End of line                       |\n| `Ctrl+W`         | Delete word before cursor         |\n| `Ctrl+U`         | Delete to start of line           |\n| `Ctrl+K`         | Delete to end of line             |\n| `Backspace`      | Delete grapheme before cursor     |\n| `Delete`         | Delete grapheme before cursor     |\n| `Alt+Backspace`  | Delete word before cursor         |\n| `Ctrl+Z`         | Undo                              |\n\n`disableArrowNavigation: true` additionally forces all nav chords (`Up`, `Down`, `Left`, `Right`, `Alt+B`, `Alt+F`, `Ctrl+A`, `Ctrl+E`) off regardless of the map.\n\n## Caveats & limitations\n\nThings to know before shipping. Most are intrinsic to running a rich editor inside a terminal.\n\n\u003Cdetails>\n\u003Csummary>\u003Cb>Environment\u003C\u002Fb>\u003C\u002Fsummary>\n\n- **Terminal-only.** Built on Ink — won't render in a browser. Requires Node 18+ for `Intl.Segmenter`. Peer deps: `ink ^7`, `react ^18 || ^19`.\n- **Monospace assumption.** Layout assumes every cell is one column wide and CJK \u002F wide chars take two. Variable-width fonts in some emulators (rare) will break alignment.\n- **Visual width via `string-width`.** Emoji + ZWJ widths follow Unicode tables; some terminals (notably older macOS Terminal, tmux without `set -g escape-time 0`) render the same glyph at a different cell count and produce off-by-one cursor placement.\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Cb>Keybinding edge cases\u003C\u002Fb>\u003C\u002Fsummary>\n\n- **Modifier+Enter detection is terminal-dependent.** `Ctrl+Enter`, `Shift+Enter`, `Alt+Enter` rely on `modifyOtherKeys` \u002F CSI-u sequences. macOS Terminal.app and Windows console don't emit them by default — use iTerm2, WezTerm, Kitty, or Alacritty, or fall back to `Ctrl+J` for newline.\n- **`Alt` on macOS.** Option key inserts special chars (`Opt+B` → `∫`) unless the terminal is set to \"Use Option as Meta\" (iTerm2: *Profiles → Keys*; Terminal.app: *Profiles → Keyboard → Use Option as Meta key*).\n- **`Tab` is silently swallowed without `onTab`.** No newline, no insert, no error. Provide the handler if you want any Tab behavior.\n- **`disableArrowNavigation` is not read-only.** Typing still mutates the buffer. Use `focus={false}` for true read-only.\n- **Multiple focused TextAreas race.** Ink's `useInput` delivers keys to every active hook; two textareas with `focus={true}` will both mutate. Gate via `focus` per instance.\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Cb>Paste & clipboard\u003C\u002Fb>\u003C\u002Fsummary>\n\n- **Bracketed paste required for one-step paste.** Terminals that don't emit `\\x1b[200~` (Windows cmd, very old emulators) deliver pastes as individual keystrokes — slow, and each char becomes its own undo step (effectively breaking `Ctrl+Z` for the paste).\n- **No programmatic clipboard.** No copy API; no setter for paste content. Anything the terminal doesn't deliver, the textarea can't see.\n- **No mouse.** Click-to-position, drag-select, scroll wheel — none of it. Cursor moves only via keys.\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Cb>Selection & search\u003C\u002Fb>\u003C\u002Fsummary>\n\n- **No selection.** Point cursor only. No range, no shift+arrow extension, no copy of a range.\n- **No find\u002Freplace.** Build it on top via controlled mode if you need it.\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Cb>Undo\u003C\u002Fb>\u003C\u002Fsummary>\n\n- **Time-grouped, not semantic.** Edits within `undoGroupDelay` (default 2.5 s) collapse into one step. On a slow machine the boundary may land mid-word.\n- **Bounded by `maxUndo`** (default 128). Older history is dropped silently.\n- **No redo.** `Ctrl+Y` \u002F `Ctrl+Shift+Z` are not bound. Add yourself if needed.\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Cb>Labels & styles\u003C\u002Fb>\u003C\u002Fsummary>\n\n- **Regex runs on every value change.** O(value × rules). Heavy patterns on 100k+ char buffers will lag — debounce externally or scope rules.\n- **Flat ranges, no nesting.** A label can't contain another label. First rule wins on overlap; later rules silently lose.\n- **Function-form `undefined` ≠ \"fall through\".** Returning `undefined` leaves the match unlabeled (renders with `text` style). It does not let the next rule try.\n- **`color` \u002F `bgColor` strings are passed straight to Ink's `\u003CText>`.** Invalid values fail silently — the terminal renders default. See the [Ink color reference](https:\u002F\u002Fgithub.com\u002Fvadimdemedes\u002Fink#color).\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Cb>Performance\u003C\u002Fb>\u003C\u002Fsummary>\n\n- **Cursor blink causes re-renders every `cursorInterval` ms (default 500).** Tall frames + slow terminals can flicker. Bump `cursorInterval` or set it high to effectively disable.\n- **`viewportLines` defaults to `floor(stdout.rows * 0.5)`.** Without virtualization, frames taller than the terminal trigger scroll-jank on every blink. The default trades render area for stability — override explicitly if you have height to spare.\n- **Visual-row recompute is O(value)** per keystroke. Acceptable for chat\u002Fcomment buffers; pathological for full-file editing of large source files.\n- **Resize listener is global.** Every TextArea instance subscribes to `stdout.resize`. Dozens of simultaneous instances will fan out resize events.\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Cb>Data model\u003C\u002Fb>\u003C\u002Fsummary>\n\n- **CRLF\u002FCR normalized to LF on paste and controlled values.** `onChange` always reports LF. If your storage layer needs CRLF, convert on save.\n- **`value.length` ≠ visual length.** Tabs count as 1 char regardless of `tabWidth`; emoji count as their UTF-16 code-unit length, not 1 grapheme. Use `Intl.Segmenter` if you need grapheme counts externally.\n- **Out-of-bounds `cursorPosition` is clamped silently.** No throw, no warning — `onCursorChange` reports the clamped value.\n- **Boundary callbacks fire on exact bounds only.** `onFirstLineUp` only fires when the cursor is *on* row 0 and ↑ is pressed; not \"the cursor moved past the top.\" Same for the other three.\n- **`onSubmit` is sync.** No promise return contract — coordinate async validation in your parent component.\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Cb>Virtualization gotchas\u003C\u002Fb>\u003C\u002Fsummary>\n\n- **Rows outside `viewportLines` are not rendered.** Any consumer-side measurement on hidden rows (`useBoxMetrics`, refs in `linePrefix`) won't fire until the row scrolls in.\n- **Wrapping happens at the measured content width.** Constrain via a parent `\u003CBox width={...}>` to wrap at a fixed column; otherwise wraps at `stdout.columns`.\n\n\u003C\u002Fdetails>\n\n## Development\n\n```bash\n# Install dependencies\npnpm install\n\n# Build\npnpm run build\n\n# Watch mode\npnpm run dev\n\n# Run tests\npnpm test\n\n# Format code\npnpm run format\n```\n\n## License\n\nMIT\n","react-ink-textarea 是一个用于 React Ink 的多行文本输入组件，支持在命令行界面实现美观的多行文本编辑。其核心功能包括多行编辑、光标导航、撤销操作以及可自定义的行前缀。该组件还提供了丰富的特性，如闪烁光标、活动行高亮显示、多行占位符、可选空白字符图形等，并且支持正则表达式或函数标签以及每个标签的样式设置。适用于需要在CLI环境中构建复杂表单或代码编辑器的应用场景，如开发工具、终端应用等。项目采用TypeScript编写，保证了严格的类型安全，并且易于测试和维护。","2026-06-11 04:05:34","CREATED_QUERY"]