# TagInput UI Component

## Overview

`TagInput` is the reusable tag entry component for Skinbase artwork flows.

It is designed for:

- Upload page
- Artwork edit page
- Admin moderation screens

The component encapsulates all tag UX behavior (chips, search, keyboard flow, AI suggestions, status hints) so pages stay thin.

## File Location

- `resources/js/components/tags/TagInput.jsx`

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string \| string[]` | required | Controlled selected tags. Can be CSV string or array. |
| `onChange` | `(tags: string[]) => void` | required | Called whenever selected tags change. |
| `suggestedTags` | `Array<string \| object>` | `[]` | AI-suggested tags shown as clickable pills. |
| `disabled` | `boolean` | `false` | Disables input and interactions. |
| `maxTags` | `number` | `15` | Maximum number of selected tags. |
| `minLength` | `number` | `2` | Minimum normalized tag length. |
| `maxLength` | `number` | `32` | Maximum normalized tag length. |
| `placeholder` | `string` | `Type tags…` | Input placeholder text. |
| `searchEndpoint` | `string` | `/api/tags/search` | Search API endpoint. |
| `popularEndpoint` | `string` | `/api/tags/popular` | Popular tags endpoint when input is empty. |

## Normalization

Tags are normalized client-side before being added:

- lowercase
- trim
- spaces → `-`
- remove unsupported characters
- collapse repeated separators
- max length = 32

Server-side normalization/validation still applies and remains authoritative.

## Keyboard & Interaction

- `Enter` → add tag
- `Comma` → add tag
- `Tab` → accept highlighted suggestion
- `Backspace` (empty input) → remove last tag
- `Escape` → close suggestion dropdown
- Paste CSV (`a, b, c`) → split and add valid tags

## Accessibility

- Suggestion dropdown uses `role="listbox"`
- Suggestions use `role="option"`
- Active item uses `aria-selected`
- Input uses `aria-expanded`, `aria-controls`, `aria-autocomplete`

## API Usage

The component performs debounced search (300ms):

- `GET /api/tags/search?q=<query>`
- `GET /api/tags/popular` (empty query)

Behavior:

- caches recent query results
- aborts outdated requests
- max 8 suggestions
- excludes already-selected tags
- shows non-blocking message when search fails

## Upload Integration Example

```jsx
<TagInput
  value={state.metadata.tags}
  onChange={(nextTags) => {
    dispatch({ type: 'SET_METADATA', payload: { tags: nextTags.join(', ') } })
  }}
  suggestedTags={props.suggested_tags || []}
  maxTags={15}
  minLength={2}
  maxLength={32}
/>
```

## Events & Save Strategy

`TagInput` itself does not persist to backend on keystrokes.

Persistence is done on save/publish boundary by page logic, e.g.:

- `PUT /api/artworks/{id}/tags`

This keeps UI responsive and avoids unnecessary API writes.
