diff --git a/src/design-system/primitives/Textarea/Textarea.module.css b/src/design-system/primitives/Textarea/Textarea.module.css
new file mode 100644
index 0000000..1c14d28
--- /dev/null
+++ b/src/design-system/primitives/Textarea/Textarea.module.css
@@ -0,0 +1,21 @@
+.textarea {
+ width: 100%;
+ padding: 6px 12px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ background: var(--bg-raised);
+ color: var(--text-primary);
+ font-family: var(--font-body);
+ font-size: 12px;
+ outline: none;
+ transition: border-color 0.15s, box-shadow 0.15s;
+ resize: vertical;
+}
+
+.textarea::placeholder { color: var(--text-faint); }
+.textarea:focus { border-color: var(--amber); box-shadow: 0 0 0 3px var(--amber-bg); }
+
+.resizeVertical { resize: vertical; }
+.resizeHorizontal { resize: horizontal; }
+.resizeNone { resize: none; }
+.resizeBoth { resize: both; }
diff --git a/src/design-system/primitives/Textarea/Textarea.test.tsx b/src/design-system/primitives/Textarea/Textarea.test.tsx
new file mode 100644
index 0000000..09807bf
--- /dev/null
+++ b/src/design-system/primitives/Textarea/Textarea.test.tsx
@@ -0,0 +1,79 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { createRef } from 'react'
+import { Textarea } from './Textarea'
+
+describe('Textarea', () => {
+ it('renders a textarea element', () => {
+ render()
+ expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument()
+ })
+
+ it('renders with default rows of 3', () => {
+ render()
+ const el = screen.getByRole('textbox')
+ expect(el).toHaveAttribute('rows', '3')
+ })
+
+ it('accepts a custom rows value', () => {
+ render()
+ expect(screen.getByRole('textbox')).toHaveAttribute('rows', '5')
+ })
+
+ it('accepts user input', async () => {
+ const user = userEvent.setup()
+ render()
+ const el = screen.getByRole('textbox')
+ await user.type(el, 'hello world')
+ expect(el).toHaveValue('hello world')
+ })
+
+ it('calls onChange when value changes', async () => {
+ const onChange = vi.fn()
+ const user = userEvent.setup()
+ render()
+ await user.type(screen.getByRole('textbox'), 'a')
+ expect(onChange).toHaveBeenCalled()
+ })
+
+ it('applies resize class for vertical (default)', () => {
+ render()
+ const el = screen.getByRole('textbox')
+ expect(el.className).toContain('resizeVertical')
+ })
+
+ it('applies resize class for none', () => {
+ render()
+ const el = screen.getByRole('textbox')
+ expect(el.className).toContain('resizeNone')
+ })
+
+ it('applies resize class for horizontal', () => {
+ render()
+ const el = screen.getByRole('textbox')
+ expect(el.className).toContain('resizeHorizontal')
+ })
+
+ it('applies resize class for both', () => {
+ render()
+ const el = screen.getByRole('textbox')
+ expect(el.className).toContain('resizeBoth')
+ })
+
+ it('forwards ref to the textarea element', () => {
+ const ref = createRef()
+ render()
+ expect(ref.current).toBeInstanceOf(HTMLTextAreaElement)
+ })
+
+ it('passes additional props to the textarea', () => {
+ render()
+ expect(screen.getByTestId('ta')).toBeDisabled()
+ })
+
+ it('applies custom className', () => {
+ render()
+ expect(screen.getByRole('textbox').className).toContain('custom')
+ })
+})
diff --git a/src/design-system/primitives/Textarea/Textarea.tsx b/src/design-system/primitives/Textarea/Textarea.tsx
new file mode 100644
index 0000000..2a8ca51
--- /dev/null
+++ b/src/design-system/primitives/Textarea/Textarea.tsx
@@ -0,0 +1,31 @@
+import styles from './Textarea.module.css'
+import { forwardRef, type TextareaHTMLAttributes } from 'react'
+
+type ResizeProp = 'vertical' | 'horizontal' | 'none' | 'both'
+
+interface TextareaProps extends TextareaHTMLAttributes {
+ resize?: ResizeProp
+}
+
+const resizeClassMap: Record = {
+ vertical: styles.resizeVertical,
+ horizontal: styles.resizeHorizontal,
+ none: styles.resizeNone,
+ both: styles.resizeBoth,
+}
+
+export const Textarea = forwardRef(
+ ({ resize = 'vertical', className, rows = 3, ...rest }, ref) => {
+ const resizeClass = resizeClassMap[resize]
+ return (
+
+ )
+ },
+)
+
+Textarea.displayName = 'Textarea'