# @neynar/ui - Complete Documentation

> React component library built on Base UI primitives + Tailwind CSS v4. This file contains complete documentation for all 53 components, theming, hooks, and utilities.

---

# Package Overview


A React component library built on Base UI primitives + Tailwind CSS v4. Production-tested in Neynar Studio.

## Quick Start

```tsx
import { Button } from "@neynar/ui/button";
import { Card, CardHeader, CardContent } from "@neynar/ui/card";
import "@neynar/ui/themes/purple-dawn";

export function App() {
  return (
    <Card>
      <CardHeader>Welcome</CardHeader>
      <CardContent>
        <Button>Get Started</Button>
      </CardContent>
    </Card>
  );
}
```

## Import Pattern

Each component has its own entry point:

```tsx
import { Button } from "@neynar/ui/button";
import { Dialog, DialogTrigger, DialogContent } from "@neynar/ui/dialog";
import { Input } from "@neynar/ui/input";
import { cn } from "@neynar/ui/utils";
```

## Themes

Two themes available:

```tsx
// Purple Dawn - elegant translucent surfaces with purple tint (default)
import "@neynar/ui/themes/purple-dawn";

// First Light - hand-drawn wireframe aesthetic
import "@neynar/ui/themes/first-light";
```

See [theming.llm.md](./theming.llm.md) for customization.

## Color Mode

SSR-safe dark mode with automatic system detection:

```tsx
import { ColorModeInitializer, ColorModeToggle } from "@neynar/ui/color-mode";

// In layout.tsx <head>
<ColorModeInitializer />

// Anywhere for user control
<ColorModeToggle />
```

## Component Categories

### Core Inputs
Button, Checkbox, Input, RadioGroup, Select, Slider, Switch, Textarea, Toggle, ToggleGroup

### Form & Field
ButtonGroup, Calendar, Field, InputGroup, InputOTP, Label

### Layout & Structure
Accordion, AspectRatio, Card, Collapsible, Resizable, Separator, Table

### Navigation & Menus
Breadcrumb, ContextMenu, DropdownMenu, Menubar, NavigationMenu, Pagination, Tabs

### Overlays & Dialogs
AlertDialog, Dialog, Drawer, HoverCard, Popover, Sheet, Tooltip

### Feedback & Status
Alert, Badge, Empty, Progress, Skeleton, Sonner (toast), Spinner

### Advanced
Avatar, Carousel, Chart, Command, Kbd, ScrollArea, Sidebar

## Documentation

- [Component Docs](./components/) - 57 individual component docs
- [Theming Guide](./theming.llm.md) - Themes, color mode, CSS variables
- [Hooks](./hooks.llm.md) - useIsMobile
- [Utilities](./utilities.llm.md) - cn() class merging

---

# Theming


@neynar/ui theming system using CSS custom properties and Tailwind CSS.

## Quick Start

Import a theme in your app's global CSS or layout:

```tsx
// Option 1: Import in CSS
import "@neynar/ui/themes/purple-dawn"

// Option 2: Import in layout.tsx
import "@neynar/ui/themes/purple-dawn"
```

## Concepts

- **Theme** = Visual aesthetic (purple-dawn, first-light) - imported via CSS
- **Color Mode** = Light or dark variant - controlled at runtime via `.dark` class

## Available Themes

| Theme | Import | Description |
|-------|--------|-------------|
| Purple Dawn | `@neynar/ui/themes/purple-dawn` | Default. Elegant translucent surfaces with purple tint |
| First Light | `@neynar/ui/themes/first-light` | Hand-drawn sketch aesthetic with wobbly edges |

## Architecture

### base.css (Infrastructure)

Shared by all themes. Contains:
- Tailwind CSS imports (`tailwindcss`, `tw-animate-css`)
- `@theme` block mapping CSS variables to Tailwind utilities
- `@source` directive for component class scanning
- Base layer styles (borders, body background)
- Surface blur rules for overlay components via `[data-slot]` selectors

**Do NOT import base.css directly.** Always import a theme.

### Theme Files

Each theme defines CSS custom properties for:
- Colors (background, foreground, semantic colors)
- Typography (font-family)
- Spacing (radius, surface-blur)
- Both light (`:root`) and dark (`.dark`) modes

## CSS Variables

### Core Colors

| Variable | Usage |
|----------|-------|
| `--background` | Page background |
| `--foreground` | Default text |
| `--card` / `--card-foreground` | Card surfaces |
| `--popover` / `--popover-foreground` | Dropdown/dialog surfaces |
| `--primary` / `--primary-foreground` | Primary buttons, links |
| `--secondary` / `--secondary-foreground` | Secondary actions |
| `--muted` / `--muted-foreground` | Subtle backgrounds, helper text |
| `--subtle-foreground` | Even lighter text (40% opacity) |
| `--accent` / `--accent-foreground` | Hover states |
| `--border` | Border colors |
| `--input` | Input borders |
| `--ring` | Focus ring |

### Semantic Colors

| Variable | Usage |
|----------|-------|
| `--destructive` | Errors, delete actions |
| `--success` | Success states |
| `--warning` | Warning states |
| `--info` | Informational states |

### Chart Colors

`--chart-1` through `--chart-5` for data visualization.

### Sidebar Colors

`--sidebar`, `--sidebar-foreground`, `--sidebar-primary`, `--sidebar-primary-foreground`, `--sidebar-accent`, `--sidebar-accent-foreground`, `--sidebar-border`, `--sidebar-ring`

### Theme-Specific Variables

| Variable | Theme | Usage |
|----------|-------|-------|
| `--surface-blur` | All | Backdrop blur amount (12px default, 4px for First Light) |
| `--font-family` | All | Primary font family |
| `--radius` | All | Border radius base (0.625rem default, 0 for First Light) |
| `--first-light-shadow` | First Light | Offset shadow effect (3px 3px) |
| `--first-light-shadow-hover` | First Light | Hover shadow (2px 2px) |
| `--first-light-shadow-active` | First Light | Active/pressed shadow (0px 0px) |

## Radius Utilities

The `--radius` variable is used to calculate multiple radius sizes via `@theme inline`:

```css
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
```

## Dark Mode

Add `.dark` class to `<html>` or a parent element:

```tsx
<html className="dark">
```

Themes define both light (`:root`) and dark (`.dark`) variants.

### Setup with ColorModeInitializer

To prevent flash of incorrect color mode:

```tsx
import { ColorModeInitializer } from "@neynar/ui/color-mode";

<html suppressHydrationWarning>
  <head>
    <ColorModeInitializer />
  </head>
  <body>{children}</body>
</html>
```

### Color Mode API

```tsx
// Get current mode
const isDark = document.documentElement.classList.contains('dark');

// Set mode programmatically
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add('dark');

// Persist preference
document.cookie = 'color-mode={"preference":"dark","mode":"dark"}; path=/; max-age=31536000';
```

## Runtime Theme Switching

For Storybook or dynamic switching, add theme class to `<html>`:

```tsx
// Switch to First Light theme
document.documentElement.classList.add("theme-first-light")
document.documentElement.classList.remove("theme-purple-dawn")

// Switch to Purple Dawn theme
document.documentElement.classList.add("theme-purple-dawn")
document.documentElement.classList.remove("theme-first-light")
```

## Purple Dawn Theme

The default theme with elegant translucent surfaces.

### Characteristics

- **Font**: Figtree Variable (sans-serif)
- **Radius**: 0.625rem (10px)
- **Surface Blur**: 12px
- **Color Tint**: Purple (hue 290)
- **Surface Opacity**: 75% for cards and popovers

### Light Mode Colors

```css
--background: oklch(0.96 0.06 290);      /* Light purple-tinted white */
--foreground: oklch(0.18 0.08 290);      /* Dark purple-tinted black */
--card: oklch(0.93 0.08 290 / 75%);      /* Translucent purple */
--primary: oklch(0.3 0.09 290);          /* Dark purple */
--border: oklch(0.18 0.08 290 / 20%);    /* 20% opacity border */
```

### Dark Mode Colors

```css
--background: oklch(0.145 0.02 290);     /* Very dark purple */
--foreground: oklch(0.985 0.01 290);     /* Near-white */
--card: oklch(0.205 0.03 290 / 75%);     /* Translucent dark purple */
--primary: oklch(0.87 0.02 290);         /* Light purple */
--border: oklch(0.985 0.01 290 / 15%);   /* 15% opacity border */
```

## First Light Theme

Hand-drawn wireframe aesthetic with wobbly SVG filters.

### Setup

First Light requires an SVG filter component for the wobbly edge effect:

```tsx
import "@neynar/ui/themes/first-light"
import { FirstLightFilters } from "@neynar/ui/first-light"

// Add once in your root layout
export function Layout({ children }) {
  return (
    <>
      <FirstLightFilters />
      {children}
    </>
  )
}
```

### Characteristics

- **Font**: Architects Daughter (handwriting)
- **Radius**: 0 (sharp corners)
- **Surface Blur**: 4px (minimal)
- **Color Style**: High contrast black/white with strong borders
- **Special Effect**: SVG turbulence filter for wobbly edges

### Light Mode (Paper)

```css
--background: #fafaf8;                   /* Warm white paper */
--foreground: #1a1a1a;                   /* Pencil black */
--border: rgba(0, 0, 0, 0.7);            /* Strong black border */
--accent: #fff3b0;                       /* Highlighter yellow */
```

### Dark Mode (Chalkboard)

```css
--background: #1e2a1e;                   /* Green-tinted chalkboard */
--foreground: #e8e8e8;                   /* Chalk white */
--border: rgba(255, 255, 255, 0.6);      /* White chalk border */
--accent: #e8d44d;                       /* Yellow chalk */
```

### First Light Utility Classes

| Class | Effect |
|-------|--------|
| `.first-light-paper` | Grid paper background (20px grid) |
| `.first-light-lined` | Lined paper background (28px lines) |
| `.first-light-highlight` | Yellow highlighter effect |
| `.first-light-underline` | Hand-drawn underline (slightly rotated) |
| `.first-light-scribble` | Dashed underline effect |
| `.first-light-light` | Lighter wobble filter intensity |
| `.first-light-heavy` | Heavier wobble filter intensity |

### SVG Filter Details

The `FirstLightFilters` component provides three filter intensities:

| Filter ID | Base Frequency | Octaves | Scale | Use Case |
|-----------|----------------|---------|-------|----------|
| `#first-light-filter` | 0.015 | 2 | 1.5 | Default wobble |
| `#first-light-filter-light` | 0.006 | 1 | 0.4 | Subtle wobble |
| `#first-light-filter-heavy` | 0.012 | 3 | 1.5 | Pronounced wobble |

## Frosted Glass Effect

Cards, popovers, dialogs, sheets, and menus use translucent backgrounds with backdrop blur:

- **Surface Opacity**: 75% on `--card` and `--popover`
- **Backdrop Blur**: Via `--surface-blur` variable (applied to `[data-slot]` elements)
- **Transparent Borders**: 10-20% opacity

Components that receive the blur effect (via `base.css`):
- `[data-slot="card"]`
- `[data-slot="popover-content"]`
- `[data-slot="hover-card-content"]`
- `[data-slot="dialog-content"]`
- `[data-slot="alert-dialog-content"]`
- `[data-slot="sheet-content"]`
- `[data-slot="drawer-content"]`
- `[data-slot="dropdown-menu-content"]`
- `[data-slot="context-menu-content"]`
- `[data-slot="menubar-content"]`
- `[data-slot="navigation-menu-popup"]`
- `[data-slot="combobox-content"]`
- `[data-slot="select-content"]`
- `[data-slot="command"]`

## Creating Custom Themes

1. Create a new CSS file importing base.css
2. Define CSS variables in `:root` and `.dark`
3. Optionally add `.theme-{name}` selector for runtime switching

```css
@import "../base.css";

:root,
html.theme-custom {
  --font-family: "Inter", sans-serif;
  --radius: 0.5rem;
  --surface-blur: 8px;
  --background: oklch(0.98 0 0);
  --foreground: oklch(0.1 0 0);
  /* ... rest of variables */
}

.dark,
html.theme-custom.dark {
  --background: oklch(0.1 0 0);
  --foreground: oklch(0.98 0 0);
  /* ... dark mode overrides */
}
```

## Color Format

Themes use **oklch()** for perceptually uniform colors:

```css
--primary: oklch(0.5 0.18 290);
/*              L    C    H
                |    |    +-- Hue (0-360)
                |    +------- Chroma (0-0.37)
                +------------ Lightness (0-1)
*/
```

Benefits:
- Consistent perceived brightness across hues
- Easy to create harmonious palettes
- Supports relative color syntax for derived colors

### Relative Color Syntax

First Light theme uses relative color syntax for toast colors:

```css
--success-bg: oklch(from var(--success) 0.92 0.05 h / 90%);
--success-text: oklch(from var(--success) 0.25 c h);
```

This derives new colors from the base `--success` color, preserving hue while adjusting lightness and chroma.

## Using Theme Tokens in Tailwind

All CSS variables are mapped to Tailwind utilities via `@theme inline` in base.css:

```tsx
<div className="bg-background text-foreground">
  <button className="bg-primary text-primary-foreground hover:bg-accent">
    Click
  </button>
</div>
```

Available color utilities: `bg-{token}`, `text-{token}`, `border-{token}`, etc.

## Customization

Override any token in your CSS:

```css
:root {
  --primary: oklch(0.6 0.25 260);  /* Custom purple */
  --radius: 0.5rem;                 /* Smaller corners */
}

.dark {
  --primary: oklch(0.7 0.2 260);
}
```

---

# Hooks


React hooks provided by @neynar/ui.

## useIsMobile

Detects mobile viewport using `matchMedia`. Returns reactive boolean.

### Import

```tsx
import { useIsMobile } from "@neynar/ui/use-mobile"
```

### Returns

| Type | Description |
|------|-------------|
| `boolean` | `true` when viewport < 768px, `false` otherwise |

### Behavior

- Returns `false` during SSR (hydration-safe)
- Updates reactively on viewport resize
- Uses `matchMedia` for performance (no resize listener spam)
- Breakpoint: 768px (CSS `md:` equivalent)

### Example: Responsive Navigation

```tsx
import { useIsMobile } from "@neynar/ui/use-mobile"

function ResponsiveNav() {
  const isMobile = useIsMobile()
  return isMobile ? <MobileNav /> : <DesktopNav />
}
```

### Example: Responsive Modal

Use Sheet for mobile, Dialog for desktop:

```tsx
import { useIsMobile } from "@neynar/ui/use-mobile"
import { Sheet, SheetContent } from "@neynar/ui/sheet"
import { Dialog, DialogContent } from "@neynar/ui/dialog"

function ResponsiveModal({ open, onOpenChange, children }) {
  const isMobile = useIsMobile()

  if (isMobile) {
    return (
      <Sheet open={open} onOpenChange={onOpenChange}>
        <SheetContent side="bottom">{children}</SheetContent>
      </Sheet>
    )
  }

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent>{children}</DialogContent>
    </Dialog>
  )
}
```

### Example: Conditional Rendering

```tsx
import { useIsMobile } from "@neynar/ui/use-mobile"

function Dashboard() {
  const isMobile = useIsMobile()

  return (
    <div className="flex">
      {!isMobile && <Sidebar />}
      <main className="flex-1">
        <Content />
      </main>
      {isMobile && <MobileBottomNav />}
    </div>
  )
}
```

### Related

- [Sheet](./components/sheet.llm.md) - Often used with mobile detection for bottom sheets
- [Drawer](./components/drawer.llm.md) - Mobile-first bottom sheet component
- [Sidebar](./components/sidebar.llm.md) - Responsive sidebar that can use mobile detection

---

# Utilities


Helper functions provided by @neynar/ui.

## cn()

Merges class names with Tailwind CSS conflict resolution.

### Import

```tsx
import { cn } from "@neynar/ui/utils"
```

### Signature

```tsx
function cn(...inputs: ClassValue[]): string
```

### How It Works

1. **clsx** - Handles conditionals, arrays, and objects
2. **tailwind-merge** - Resolves Tailwind conflicts (last class wins)

### Examples

```tsx
// Conditional classes
cn("px-4", isActive && "bg-primary")

// Merge with className prop
cn("rounded-lg border", className)

// Tailwind conflict resolution
cn("text-red-500", "text-blue-500") // → "text-blue-500"
cn("px-4 py-2", "px-8") // → "px-8 py-2"

// Objects and arrays
cn({ "opacity-50": disabled }, ["flex", "items-center"])
```

### Common Patterns

```tsx
// Component with className prop
function Card({ className, ...props }) {
  return (
    <div className={cn("rounded-lg border bg-card", className)} {...props} />
  )
}

// Conditional styling
<Button className={cn(isLoading && "opacity-50 cursor-wait")}>
  Submit
</Button>

// Variant-based styling
<div className={cn(
  "p-4",
  variant === "destructive" && "bg-destructive text-destructive-foreground",
  variant === "success" && "bg-success text-success-foreground"
)}>
  {children}
</div>
```

---

## Internal Utilities

The following utilities are used internally by components but are **not exported**:

### CVA Variants (lib/variants.ts)

Internal variant definitions for consistent styling across components:

- **menuItemVariants** - Styling for DropdownMenuItem, ContextMenuItem, MenubarItem
- **typographyColorVariants** - Color options for Title, Text, Code, Blockquote
- **titleVariants** - Size and weight options for Title
- **textVariants** - Size, weight, alignment for Text

These are exposed via component props rather than direct imports:

```tsx
// Use component props (correct)
<Text color="muted" size="sm">Helper text</Text>
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>

// Don't try to import variants directly (not exported)
// import { menuItemVariants } from "@neynar/ui/lib/variants" // ❌
```

## Related

- [Theming](./theming.llm.md) - CSS variables and themes
- [Hooks](./hooks.llm.md) - React hooks

---

# Contributing Guide


Technical reference for AI assistants modifying this package.

## Critical Rules

1. **Never export defaults** - Named exports only
2. **Always add `data-slot`** - Root element of every component
3. **Use `type` not `interface`** - For prop definitions
4. **`"use client"` only when needed** - Only for components using React hooks
5. **Run type-check** - `yarn type-check` must pass before committing

## File Locations

```
src/
├── components/
│   ├── ui/                    # Base UI components
│   │   ├── button.tsx
│   │   └── stories/           # Storybook stories
│   │       └── button.stories.tsx
│   └── neynar/                # Neynar-specific components
│       ├── typography/
│       ├── color-mode/
│       └── first-light/
├── hooks/                     # Custom hooks
├── lib/                       # Utilities (cn, variants)
└── styles/                    # CSS and themes
    └── themes/

llm/                          # LLM documentation
├── components/                # Per-component docs
├── index.llm.md              # Package overview
├── hooks.llm.md
├── utilities.llm.md
└── theming.llm.md

package.json                   # Exports map (auto-generated)
```

## Component Template

```tsx
// Add "use client" ONLY if component uses hooks
import { Primitive } from "@base-ui/react/primitive"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const componentVariants = cva("base-classes", {
  variants: {
    variant: {
      default: "...",
      secondary: "...",
    },
  },
  defaultVariants: {
    variant: "default",
  },
})

type ComponentProps = Primitive.Props & VariantProps<typeof componentVariants>

/**
 * Brief description.
 *
 * @example
 * ```tsx
 * <Component>Content</Component>
 * ```
 */
function Component({ className, variant, ...props }: ComponentProps) {
  return (
    <Primitive
      data-slot="component"
      className={cn(componentVariants({ variant, className }))}
      {...props}
    />
  )
}

export { Component, componentVariants, type ComponentProps }
```

## Story Template

```tsx
import type { Meta, StoryObj } from "@storybook/react"
import { Component } from "../component"

const meta: Meta<typeof Component> = {
  title: "UI/Component",
  component: Component,
  tags: ["autodocs"],
}

export default meta
type Story = StoryObj<typeof Component>

export const Default: Story = {
  args: { children: "Example" },
}

export const Variants: Story = {
  render: () => (
    <div className="flex gap-4">
      <Component variant="default">Default</Component>
      <Component variant="secondary">Secondary</Component>
    </div>
  ),
}
```

## LLM Documentation Template

```markdown
# ComponentName

One sentence description.

## Import

\`\`\`tsx
import { Component } from "@neynar/ui/component"
\`\`\`

## Usage

\`\`\`tsx
<Component>Content</Component>
\`\`\`

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "default" \| "secondary" | "default" | Visual style |

## Examples

### With variant
\`\`\`tsx
<Component variant="secondary">Secondary</Component>
\`\`\`
```

## data-slot Convention

Every component root element MUST have `data-slot`:

```tsx
// Single component
<Button data-slot="button" />

// Compound component
<Dialog data-slot="dialog">
  <DialogTrigger data-slot="dialog-trigger" />
  <DialogContent data-slot="dialog-content">
    <DialogHeader data-slot="dialog-header" />
    <DialogTitle data-slot="dialog-title" />
    <DialogDescription data-slot="dialog-description" />
    <DialogFooter data-slot="dialog-footer" />
  </DialogContent>
</Dialog>
```

## "use client" Rules

**ADD** when component:
- Uses `useState`, `useEffect`, `useRef`, `useContext`
- Uses custom hooks (`useColorMode`, `useMobile`)
- Has event handlers that need client hydration

**OMIT** when component:
- Is a pure wrapper around Base UI primitive
- Only passes props through
- Has no React hooks

Current components with `"use client"`:
- avatar, calendar, carousel, chart, collapsible
- combobox, command, dialog, drawer, field
- hover-card, input-otp, label, progress, resizable
- select, sidebar, slider, switch, tabs
- toggle-group, tooltip
- color-mode/*, first-light/*

## Semantic Color Tokens

```css
/* Backgrounds */
bg-background, bg-card, bg-popover, bg-muted, bg-accent

/* Foregrounds */
text-foreground, text-muted-foreground, text-card-foreground

/* Status */
bg-primary, text-primary-foreground
bg-secondary, text-secondary-foreground
bg-destructive, text-destructive
bg-success, text-success
bg-warning, text-warning
bg-info, text-info

/* Borders */
border-border, border-input, border-ring
```

## Package.json Exports

Exports are auto-generated by `scripts/generate-exports.ts` during build.

Each component gets an entry:
```json
{
  "./button": {
    "types": "./dist/components/ui/button.d.ts",
    "import": "./dist/components/ui/button.js"
  }
}
```

## Validation Commands

```bash
# Must pass before commit
yarn type-check    # TypeScript
yarn lint          # ESLint
yarn build         # Build check

# Development
yarn storybook     # Visual testing
```

## Common Patterns

### Polymorphism with `render` prop
```tsx
<Button render={<a href="/link" />}>Link Button</Button>
```

### Compound Components
```tsx
<Dialog>
  <DialogTrigger>Open</DialogTrigger>
  <DialogContent>
    <DialogTitle>Title</DialogTitle>
    <DialogDescription>Description</DialogDescription>
  </DialogContent>
</Dialog>
```

### Forwarding refs (handled by Base UI)
```tsx
// Base UI handles ref forwarding automatically
// No need for forwardRef wrapper
function Component(props: ComponentProps) {
  return <Primitive {...props} />
}
```

### Icon sizing
```tsx
// Icons auto-size via [&_svg:not([class*='size-'])]:size-4
<Button>
  <PlusIcon /> {/* Inherits size-4 */}
  Add Item
</Button>
```

## Debugging Tips

- Check `data-slot` attributes in DevTools for component boundaries
- Use `?path=/story/ui-button--default` in Storybook URL
- Run `yarn type-check 2>&1 | head -20` for focused error output

---

# Components

# Accordion

Collapsible content sections with expand/collapse controls for FAQs, documentation, and hierarchical content.

## Import

```tsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@neynar/ui/accordion"
```

## Anatomy

```tsx
<Accordion>
  <AccordionItem value="item-1">
    <AccordionTrigger>Question or title</AccordionTrigger>
    <AccordionContent>Answer or content</AccordionContent>
  </AccordionItem>
</Accordion>
```

## Components

| Component | Description |
|-----------|-------------|
| Accordion | Root container, manages open/closed state and keyboard navigation |
| AccordionItem | Groups a trigger with its content panel, requires unique `value` |
| AccordionTrigger | Button that toggles the item, includes automatic chevron icons |
| AccordionContent | Collapsible panel with automatic slide animation |

## Props

### Accordion

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| defaultValue | any[] | - | Uncontrolled initial open items (single value or array) |
| value | any[] | - | Controlled open items (for managing state externally) |
| onValueChange | (value: any[]) => void | - | Called when items open/close |
| multiple | boolean | false | Allow multiple items open simultaneously |
| disabled | boolean | false | Disable all items |
| orientation | 'vertical' \| 'horizontal' | 'vertical' | Visual orientation, affects keyboard navigation |
| loopFocus | boolean | true | Loop focus when reaching end with arrow keys |

### AccordionItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | any | (auto) | Unique identifier for this item (required for controlled state) |
| disabled | boolean | false | Disable this specific item |
| onOpenChange | (open: boolean) => void | - | Called when this item opens/closes |

### AccordionTrigger

Accepts all Base UI Accordion.Trigger props. Automatically includes chevron icons that flip based on open state.

### AccordionContent

Accepts all Base UI Accordion.Panel props. Automatically animates height with slide down/up transitions.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| keepMounted | boolean | false | Keep content in DOM when closed (for SEO/hydration) |
| hiddenUntilFound | boolean | false | Use `hidden="until-found"` for browser's find-in-page |

## Data Attributes

All components support data attributes for styling:

| Attribute | When Present |
|-----------|--------------|
| data-open | Item/panel is expanded |
| data-closed | Item/panel is collapsed |
| data-disabled | Item/accordion is disabled |
| data-orientation | Current orientation ('vertical' or 'horizontal') |
| data-index | Index of the item in the accordion |
| data-starting-style | Panel is animating in |
| data-ending-style | Panel is animating out |

## Examples

### Single Item Open (Default)

```tsx
<Accordion defaultValue={["faq-1"]}>
  <AccordionItem value="faq-1">
    <AccordionTrigger>How do I get started?</AccordionTrigger>
    <AccordionContent>
      Sign up for a free account and generate your API key from the dashboard.
    </AccordionContent>
  </AccordionItem>

  <AccordionItem value="faq-2">
    <AccordionTrigger>What are the rate limits?</AccordionTrigger>
    <AccordionContent>
      Free tier: 1,000 requests/day. Pro: 100,000 requests/day.
    </AccordionContent>
  </AccordionItem>
</Accordion>
```

### Multiple Items Open

```tsx
<Accordion defaultValue={["item-1", "item-2"]}>
  <AccordionItem value="item-1">
    <AccordionTrigger>API Features</AccordionTrigger>
    <AccordionContent>
      Real-time feeds, webhooks, and comprehensive user data.
    </AccordionContent>
  </AccordionItem>

  <AccordionItem value="item-2">
    <AccordionTrigger>Authentication</AccordionTrigger>
    <AccordionContent>
      Sign-in with Farcaster (SIWF) and API key authentication.
    </AccordionContent>
  </AccordionItem>
</Accordion>
```

### Controlled State

```tsx
function ControlledAccordion() {
  const [openItems, setOpenItems] = useState(["item-1"])

  return (
    <Accordion value={openItems} onValueChange={setOpenItems}>
      <AccordionItem value="item-1">
        <AccordionTrigger>Controlled Item</AccordionTrigger>
        <AccordionContent>
          Open state is managed externally via React state.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

### With Icons and Rich Content

```tsx
<Accordion>
  <AccordionItem value="feature-1">
    <AccordionTrigger>
      <div className="flex items-center gap-3">
        <div className="bg-primary/10 text-primary rounded-md p-1.5">
          <ZapIcon className="size-4" />
        </div>
        <span>Real-time Updates</span>
      </div>
    </AccordionTrigger>
    <AccordionContent>
      <p>Access real-time feeds with sub-second latency.</p>
      <ul className="list-disc pl-5 space-y-1">
        <li>Automatic pagination</li>
        <li>Advanced filtering</li>
      </ul>
    </AccordionContent>
  </AccordionItem>
</Accordion>
```

### Disabled Items

```tsx
<Accordion>
  <AccordionItem value="active">
    <AccordionTrigger>Active Item</AccordionTrigger>
    <AccordionContent>This item can be toggled.</AccordionContent>
  </AccordionItem>

  <AccordionItem value="disabled" disabled>
    <AccordionTrigger>Disabled Item</AccordionTrigger>
    <AccordionContent>This content cannot be accessed.</AccordionContent>
  </AccordionItem>
</Accordion>
```

## Keyboard

| Key | Action |
|-----|--------|
| Space / Enter | Toggle focused item |
| Tab | Move to next focusable element |
| Shift + Tab | Move to previous focusable element |
| ArrowDown | Move focus to next item trigger (vertical) |
| ArrowUp | Move focus to previous item trigger (vertical) |
| Home | Focus first item trigger |
| End | Focus last item trigger |

## Accessibility

- Full keyboard navigation with arrow keys and roving tabindex
- ARIA attributes automatically applied (`aria-expanded`, `aria-controls`, `aria-labelledby`)
- Screen readers announce expanded/collapsed state changes
- Focus management maintains expected tab order
- Supports `hiddenUntilFound` for browser find-in-page functionality

## Related

- [Collapsible](./collapsible.llm.md) - Single collapsible section without accordion semantics
- [Tabs](./tabs.llm.md) - Alternative for showing one section at a time
- [Dialog](./dialog.llm.md) - For overlay content instead of inline expansion

---

# AlertDialog

Modal dialog for critical confirmations requiring user attention, typically for destructive or irreversible actions.

## Import

```tsx
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@neynar/ui/alert-dialog"
```

## Anatomy

```tsx
<AlertDialog>
  <AlertDialogTrigger />
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogMedia />
      <AlertDialogTitle />
      <AlertDialogDescription />
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel />
      <AlertDialogAction />
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
```

## Components

| Component | Description |
|-----------|-------------|
| AlertDialog | Root container managing open state and accessibility |
| AlertDialogTrigger | Button that opens the dialog (supports `render` prop) |
| AlertDialogContent | Main dialog content with portal and backdrop |
| AlertDialogHeader | Container for title, description, and optional media |
| AlertDialogTitle | Dialog title (automatically announced to screen readers) |
| AlertDialogDescription | Description text with automatic link styling |
| AlertDialogMedia | Optional icon container for visual indicators |
| AlertDialogFooter | Container for action buttons |
| AlertDialogAction | Primary action button (inherits Button variants) |
| AlertDialogCancel | Cancel button that closes the dialog |
| AlertDialogPortal | Portal container (automatically used by AlertDialogContent) |
| AlertDialogOverlay | Backdrop overlay (automatically used by AlertDialogContent) |

## Props

### AlertDialog

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | - | Controlled open state |
| defaultOpen | boolean | false | Uncontrolled default open state |
| onOpenChange | (open: boolean) => void | - | Called when open state changes |

### AlertDialogContent

Automatically renders portal and backdrop overlay. Centered with fade + zoom animations.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| size | "default" \| "sm" | "default" | Dialog size variant |

**Size Behavior:**
- `sm` - Compact dialog, footer buttons in 2-column grid on mobile
- `default` - Standard size, left-aligned on desktop when using media icon

### AlertDialogTrigger

Supports `render` prop to customize the trigger element:

```tsx
<AlertDialogTrigger render={<Button variant="destructive" />}>
  Delete Item
</AlertDialogTrigger>
```

### AlertDialogCancel

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | Button variant | "outline" | Button variant (inherits from Button) |
| size | Button size | "default" | Button size (inherits from Button) |

Automatically renders as a Button via `render` prop. Closes dialog when clicked.

### AlertDialogAction

Inherits all Button props and variants. Does not automatically close the dialog - handle close in `onClick`:

```tsx
<AlertDialogAction
  variant="destructive"
  onClick={() => {
    deleteItem()
    // Dialog closes via state change
  }}
>
  Delete
</AlertDialogAction>
```

## Data Attributes

| Attribute | When Present |
|-----------|--------------|
| data-open | Dialog is open |
| data-closed | Dialog is closed |
| data-size | Size variant ("default" or "sm") |
| data-slot | Component identifier for styling |

## Examples

### Basic Destructive Action

```tsx
<AlertDialog>
  <AlertDialogTrigger render={<Button variant="destructive" />}>
    Delete Item
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Delete this item?</AlertDialogTitle>
      <AlertDialogDescription>
        This action cannot be undone. The item will be permanently removed.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction variant="destructive">Delete</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
```

### With Media Icon

```tsx
<AlertDialog>
  <AlertDialogTrigger render={<Button variant="destructive" />}>
    Revoke Key
  </AlertDialogTrigger>
  <AlertDialogContent size="default">
    <AlertDialogHeader>
      <AlertDialogMedia>
        <ShieldAlertIcon className="text-destructive" />
      </AlertDialogMedia>
      <AlertDialogTitle>Revoke API Key?</AlertDialogTitle>
      <AlertDialogDescription>
        This will immediately invalidate the API key. Any applications using
        this key will stop working. This action cannot be undone.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction variant="destructive">Revoke Key</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
```

### Small Dialog for Quick Confirmations

```tsx
<AlertDialog>
  <AlertDialogTrigger render={<Button variant="destructive" />}>
    Delete Webhook
  </AlertDialogTrigger>
  <AlertDialogContent size="sm">
    <AlertDialogHeader>
      <AlertDialogMedia>
        <AlertTriangleIcon className="text-destructive" />
      </AlertDialogMedia>
      <AlertDialogTitle>Delete webhook?</AlertDialogTitle>
      <AlertDialogDescription>
        You will no longer receive events at this endpoint.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction variant="destructive">Delete</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
```

### Controlled State

```tsx
function ControlledExample() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button onClick={() => setOpen(true)}>Open Dialog</Button>

      <AlertDialog open={open} onOpenChange={setOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Confirm action</AlertDialogTitle>
            <AlertDialogDescription>
              This dialog is controlled externally.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel onClick={() => setOpen(false)}>
              Cancel
            </AlertDialogCancel>
            <AlertDialogAction
              onClick={() => {
                performAction()
                setOpen(false)
              }}
            >
              Confirm
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  )
}
```

### Complex Content with Lists

```tsx
<AlertDialog>
  <AlertDialogTrigger render={<Button variant="destructive" />}>
    Delete Multiple Items
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogMedia>
        <TrashIcon className="text-destructive" />
      </AlertDialogMedia>
      <AlertDialogTitle>Delete 3 items?</AlertDialogTitle>
      <AlertDialogDescription>
        The following items will be permanently deleted:
        <ul className="mt-2 list-inside list-disc space-y-1">
          <li>API Key: prod_****_8f3d</li>
          <li>Webhook: https://api.example.com/hook</li>
          <li>Team Member: jane@example.com</li>
        </ul>
        <p className="mt-2">This action cannot be undone.</p>
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction variant="destructive">Delete All</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
```

## Keyboard

| Key | Action |
|-----|--------|
| Escape | Close dialog |
| Tab | Navigate between action and cancel buttons |
| Space/Enter | Activate focused button |

## Accessibility

- Title is announced to screen readers via `aria-labelledby`
- Description is associated via `aria-describedby`
- Focus is trapped within dialog when open
- Focus returns to trigger element when closed
- Escape key closes dialog
- Backdrop click closes dialog

## Related

- [Dialog](./dialog.llm.md) - For non-critical confirmations and general content
- [Button](./button.llm.md) - Used for action and cancel buttons
- [Popover](./popover.llm.md) - For non-modal contextual content

---

# Alert

Displays contextual messages with optional icons and actions for notifications, warnings, errors, and informational content.

## Import

```tsx
import { Alert, AlertTitle, AlertDescription, AlertAction } from "@neynar/ui/alert"
```

## Anatomy

```tsx
<Alert variant="default">
  <InfoIcon />
  <AlertTitle>Title</AlertTitle>
  <AlertDescription>Description with optional links.</AlertDescription>
  <AlertAction>
    <Button size="xs">Action</Button>
  </AlertAction>
</Alert>
```

## Components

| Component | Description |
|-----------|-------------|
| Alert | Root container with variant styling and grid layout |
| AlertTitle | Title heading, auto-positions next to icon |
| AlertDescription | Description content with muted foreground color |
| AlertAction | Action area positioned in top-right corner |

## Props

### Alert

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "default" \| "destructive" \| "success" \| "warning" \| "info" | "default" | Visual style variant for different message types |
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Alert content (icon, title, description, action) |

All standard HTML div props are supported.

### AlertTitle

Standard HTML div props. Automatically positions next to icon when present using CSS grid. Supports inline links with underline styling on hover.

### AlertDescription

Standard HTML div props. Uses muted foreground color that adapts to the alert variant. Supports paragraph spacing and inline links.

### AlertAction

Standard HTML div props. Absolutely positioned in top-right corner (top-2.5, right-3). Use for dismiss buttons or action CTAs.

## Data Attributes

| Attribute | Element | When Present |
|-----------|---------|--------------|
| data-slot="alert" | Alert | Always on root alert element |
| data-slot="alert-title" | AlertTitle | Always on title element |
| data-slot="alert-description" | AlertDescription | Always on description element |
| data-slot="alert-action" | AlertAction | Always on action element |

The Alert uses `has-data-[slot=alert-action]:pr-18` to add right padding when an action is present, preventing content overlap.

## Variants

| Variant | Usage |
|---------|-------|
| default | General information, updates, neutral notifications |
| destructive | Errors, critical information requiring immediate attention |
| success | Successful actions, positive outcomes, confirmations |
| warning | Important warnings requiring attention but not critical |
| info | Informational messages, tips, helpful context |

## Icon Handling

When an SVG icon is included as a direct child:
- Alert switches to 2-column grid layout (`has-[>svg]:grid-cols-[auto_1fr]`)
- Icon is auto-sized to 16px (`*:[svg:not([class*='size-'])]:size-4`)
- Icon color matches variant text color
- Icon spans both rows and translates slightly down for alignment
- Title and description automatically position in second column

## Examples

### Basic Alert

```tsx
<Alert>
  <InfoIcon />
  <AlertTitle>Heads up!</AlertTitle>
  <AlertDescription>
    You can add components to your app using the CLI.
  </AlertDescription>
</Alert>
```

### Destructive/Error Alert

```tsx
<Alert variant="destructive">
  <AlertTriangleIcon />
  <AlertTitle>Authentication Error</AlertTitle>
  <AlertDescription>
    Your API key has expired or is invalid. Please generate a new key or check
    your credentials in the <a href="#settings">API settings</a> page.
  </AlertDescription>
</Alert>
```

### Success Alert

```tsx
<Alert variant="success">
  <CheckCircle2Icon />
  <AlertTitle>Plan Upgrade Successful</AlertTitle>
  <AlertDescription>
    Your account has been upgraded to the Pro plan. Your new rate limits and
    features are now active.
  </AlertDescription>
</Alert>
```

### With Dismiss Action

```tsx
<Alert variant="destructive">
  <AlertTriangleIcon />
  <AlertTitle>Rate Limit Warning</AlertTitle>
  <AlertDescription>
    You've used 95% of your API rate limit for this billing period. Your requests
    may be throttled. Consider <a href="#upgrade">upgrading your plan</a> to
    increase your limit.
  </AlertDescription>
  <AlertAction>
    <Button variant="ghost" size="icon-xs" aria-label="Dismiss">
      <XIcon />
    </Button>
  </AlertAction>
</Alert>
```

### With Primary Action Button

```tsx
<Alert>
  <InfoIcon />
  <AlertTitle>API v1 Deprecation Notice</AlertTitle>
  <AlertDescription>
    The v1 API endpoints will be deprecated on March 31, 2025. Please migrate to
    <a href="#migration">v2 endpoints</a> to avoid service disruption.
  </AlertDescription>
  <AlertAction>
    <Button size="xs" variant="outline">
      View Migration Guide
    </Button>
  </AlertAction>
</Alert>
```

### Description Only (No Title)

```tsx
<Alert>
  <InfoIcon />
  <AlertDescription>
    Your changes have been saved automatically.
  </AlertDescription>
</Alert>
```

### Without Icon

```tsx
<Alert>
  <AlertTitle>Simple Alert</AlertTitle>
  <AlertDescription>
    This alert doesn't have an icon, useful for less critical information.
  </AlertDescription>
</Alert>
```

### Warning Alert

```tsx
<Alert variant="warning">
  <AlertTriangleIcon />
  <AlertTitle>Webhook Configuration Required</AlertTitle>
  <AlertDescription>
    Set up webhooks to receive real-time notifications for cast events, reactions,
    and follows. <a href="#webhooks">Configure webhooks</a> in your dashboard settings.
  </AlertDescription>
</Alert>
```

### Info Alert

```tsx
<Alert variant="info">
  <BellIcon />
  <AlertTitle>Update Available</AlertTitle>
  <AlertDescription>
    A new version of the API is available with improved performance.
  </AlertDescription>
  <AlertAction>
    <Button size="xs">Learn More</Button>
  </AlertAction>
</Alert>
```

## Styling

### Custom Variants

Use the `alertVariants` function to extend or create custom variants:

```tsx
import { alertVariants } from "@neynar/ui/alert"

<Alert className={alertVariants({ variant: "success" })}>
  {/* ... */}
</Alert>
```

### Custom Styling

Override styles using className:

```tsx
<Alert className="border-2 shadow-lg">
  <AlertTitle className="text-lg">Custom Styled</AlertTitle>
  <AlertDescription className="text-base">
    With larger text and border.
  </AlertDescription>
</Alert>
```

## Accessibility

- Uses `role="alert"` for screen reader announcements
- Alert content is announced immediately when rendered
- Interactive elements (links, buttons) are keyboard accessible
- Link underlines and hover states provide clear affordance
- Variant colors provide additional visual context beyond text

## Layout Behavior

The Alert uses CSS Grid for flexible layout:
- **Without icon**: Single column layout
- **With icon**: Two-column grid with icon in first column
- **With action**: Adds right padding to prevent overlap
- **Responsive text**: Uses `text-balance` on mobile and `text-pretty` on desktop for optimal readability

## Common Patterns

### API Dashboard Notifications

```tsx
<div className="space-y-4">
  <Alert variant="destructive">
    <AlertTriangleIcon />
    <AlertTitle>Rate Limit Warning</AlertTitle>
    <AlertDescription>
      You've used 95% of your API rate limit for this billing period.
    </AlertDescription>
    <AlertAction>
      <Button variant="ghost" size="icon-xs">
        <XIcon />
      </Button>
    </AlertAction>
  </Alert>

  <Alert>
    <InfoIcon />
    <AlertTitle>API v1 Deprecation Notice</AlertTitle>
    <AlertDescription>
      The v1 API endpoints will be deprecated on March 31, 2025.
    </AlertDescription>
    <AlertAction>
      <Button size="xs" variant="outline">
        Learn More
      </Button>
    </AlertAction>
  </Alert>
</div>
```

### Inline Links

Alert descriptions automatically style links with underlines and hover states:

```tsx
<Alert>
  <InfoIcon />
  <AlertTitle>Documentation Updated</AlertTitle>
  <AlertDescription>
    We've updated our API documentation with new examples and best practices.
    <a href="#docs">View the updated docs</a> or <a href="#changelog">read the changelog</a>.
  </AlertDescription>
</Alert>
```

## Related

- **Toast** - For temporary notifications that auto-dismiss
- **Banner** - For persistent site-wide announcements
- **Dialog** - For alerts requiring user confirmation

---

# AspectRatio

Container that maintains a consistent aspect ratio, automatically adjusting height based on width.

## Import

```tsx
import { AspectRatio } from "@neynar/ui/aspect-ratio"
```

## Anatomy

```tsx
<AspectRatio ratio={16 / 9}>
  <img src="..." alt="..." />
</AspectRatio>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| ratio | number | - | Aspect ratio as width/height (e.g., 16/9, 1, 4/3) |
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Content to display within aspect ratio container |

All standard `div` props are supported.

## Common Aspect Ratios

| Ratio | Value | Use Case |
|-------|-------|----------|
| 16:9 | 16/9 | Video content, YouTube thumbnails, widescreen |
| 4:3 | 4/3 | Traditional displays, some photography |
| 1:1 | 1 | Square images, Instagram posts, NFTs, profile pictures |
| 21:9 | 21/9 | Ultrawide/cinematic content, hero banners |
| 9:16 | 9/16 | Vertical video (stories, reels) |
| 2:1 | 2/1 | Panoramic images, Twitter banners |

## Examples

### Video Thumbnail

```tsx
<AspectRatio ratio={16 / 9} className="overflow-hidden rounded-md">
  <img
    src="/video-thumbnail.jpg"
    alt="Video thumbnail"
    className="size-full object-cover"
  />
  <div className="absolute inset-0 flex items-center justify-center bg-black/30">
    <PlayIcon className="size-12" />
  </div>
</AspectRatio>
```

### NFT Gallery

```tsx
<div className="grid grid-cols-4 gap-4">
  {nfts.map((nft) => (
    <AspectRatio key={nft.id} ratio={1} className="overflow-hidden rounded-md">
      <img
        src={nft.image}
        alt={nft.name}
        className="size-full object-cover hover:scale-110 transition-transform"
      />
    </AspectRatio>
  ))}
</div>
```

### Profile Banner

```tsx
<AspectRatio ratio={21 / 9} className="overflow-hidden">
  <img
    src="/banner.jpg"
    alt="Profile banner"
    className="size-full object-cover"
  />
  <div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
  <div className="absolute bottom-6 left-6 text-white">
    <h3 className="text-2xl font-bold">Channel Name</h3>
  </div>
</AspectRatio>
```

### Placeholder Content

```tsx
<AspectRatio ratio={16 / 9} className="bg-muted flex items-center justify-center rounded-md">
  <div className="text-center">
    <ImageIcon className="size-12 text-muted-foreground" />
    <p className="text-sm">Upload Image</p>
  </div>
</AspectRatio>
```

## Usage Notes

- Always use `overflow-hidden` with images to prevent content overflow
- Combine with `object-cover` for images that fill the container
- Use `object-contain` to fit entire image with possible letterboxing
- Container width is flexible; height is calculated automatically
- Works with any content type: images, video, gradients, custom elements

## Related

- [Card](./card.llm.md) - Often used together for media cards

---

# Avatar

Display user profile images with automatic fallback support, status badges, and grouping.

## Import

```tsx
import {
  Avatar,
  AvatarImage,
  AvatarFallback,
  AvatarBadge,
  AvatarGroup,
  AvatarGroupCount,
} from "@neynar/ui/avatar"
```

## Anatomy

```tsx
<Avatar size="default">
  <AvatarImage src="/user.jpg" alt="Username" />
  <AvatarFallback>UN</AvatarFallback>
  <AvatarBadge>
    <CheckIcon />
  </AvatarBadge>
</Avatar>

<AvatarGroup>
  <Avatar>...</Avatar>
  <Avatar>...</Avatar>
  <AvatarGroupCount>+5</AvatarGroupCount>
</AvatarGroup>
```

## Components

| Component | Description |
|-----------|-------------|
| Avatar | Root container with size variants |
| AvatarImage | Image element with automatic fallback |
| AvatarFallback | Fallback content (initials or icon) shown when image unavailable |
| AvatarBadge | Status indicator positioned at bottom-right |
| AvatarGroup | Container for stacked avatars with ring borders |
| AvatarGroupCount | Counter badge showing overflow count ("+N") |

## Props

### Avatar

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| size | "sm" \| "default" \| "lg" | "default" | Avatar size variant |

Extends all Base UI Avatar.Root props.

### AvatarImage

Standard `img` element props via Base UI Avatar.Image.

| Prop | Type | Description |
|------|------|-------------|
| src | string | Image source URL |
| alt | string | Alternative text for accessibility |

### AvatarFallback

Extends Base UI Avatar.Fallback props. Accepts text content (typically initials) or icon elements.

### AvatarBadge

Standard `span` element props. Can contain icons or be used as colored dot.

### AvatarGroup

Standard `div` element props. Automatically applies ring borders and negative spacing to child avatars.

### AvatarGroupCount

Standard `div` element props. Display text like "+12" or icons.

## Sizes

| Size | Dimensions | Use Case |
|------|------------|----------|
| sm | 24px | Compact lists, inline mentions |
| default | 32px | Standard UI, activity feeds |
| lg | 40px | Profile headers, featured users |

## Data Attributes

| Attribute | Value | Applied To |
|-----------|-------|------------|
| data-slot | "avatar" | Avatar root |
| data-slot | "avatar-image" | AvatarImage |
| data-slot | "avatar-fallback" | AvatarFallback |
| data-slot | "avatar-badge" | AvatarBadge |
| data-slot | "avatar-group" | AvatarGroup |
| data-slot | "avatar-group-count" | AvatarGroupCount |
| data-size | "sm" \| "default" \| "lg" | Avatar root (for size styling) |

## Examples

### Basic Usage

```tsx
<Avatar>
  <AvatarImage src="https://i.imgur.com/xIe7Wlb.png" alt="@dwr.eth" />
  <AvatarFallback>DW</AvatarFallback>
</Avatar>
```

### With Sizes

```tsx
<Avatar size="sm">
  <AvatarImage src="/user.jpg" alt="User" />
  <AvatarFallback>AB</AvatarFallback>
</Avatar>

<Avatar size="default">
  <AvatarImage src="/user.jpg" alt="User" />
  <AvatarFallback>AB</AvatarFallback>
</Avatar>

<Avatar size="lg">
  <AvatarImage src="/user.jpg" alt="User" />
  <AvatarFallback>AB</AvatarFallback>
</Avatar>
```

### Fallback States

```tsx
{/* Initials fallback */}
<Avatar>
  <AvatarFallback>JD</AvatarFallback>
</Avatar>

{/* Icon fallback */}
<Avatar>
  <AvatarFallback>
    <UserIcon className="size-4" />
  </AvatarFallback>
</Avatar>
```

### With Status Badge

```tsx
{/* Verification badge */}
<Avatar>
  <AvatarImage src="/user.jpg" alt="Verified user" />
  <AvatarFallback>VU</AvatarFallback>
  <AvatarBadge>
    <CheckIcon />
  </AvatarBadge>
</Avatar>

{/* Online status dot */}
<Avatar>
  <AvatarImage src="/user.jpg" alt="Online user" />
  <AvatarFallback>OU</AvatarFallback>
  <AvatarBadge className="bg-green-500" />
</Avatar>

{/* Custom status */}
<Avatar>
  <AvatarFallback>PU</AvatarFallback>
  <AvatarBadge className="bg-yellow-500">
    <ZapIcon />
  </AvatarBadge>
</Avatar>
```

### Avatar Group

```tsx
{/* Basic group */}
<AvatarGroup>
  <Avatar>
    <AvatarImage src="/user1.jpg" alt="User 1" />
    <AvatarFallback>U1</AvatarFallback>
  </Avatar>
  <Avatar>
    <AvatarImage src="/user2.jpg" alt="User 2" />
    <AvatarFallback>U2</AvatarFallback>
  </Avatar>
  <Avatar>
    <AvatarFallback>U3</AvatarFallback>
  </Avatar>
</AvatarGroup>

{/* With overflow count */}
<AvatarGroup>
  <Avatar size="sm">
    <AvatarImage src="/user1.jpg" alt="User 1" />
    <AvatarFallback>U1</AvatarFallback>
  </Avatar>
  <Avatar size="sm">
    <AvatarImage src="/user2.jpg" alt="User 2" />
    <AvatarFallback>U2</AvatarFallback>
  </Avatar>
  <Avatar size="sm">
    <AvatarFallback>U3</AvatarFallback>
  </Avatar>
  <AvatarGroupCount>+8</AvatarGroupCount>
</AvatarGroup>

{/* Icon with count */}
<AvatarGroup>
  <Avatar>
    <AvatarFallback>
      <UsersIcon className="size-4" />
    </AvatarFallback>
  </Avatar>
  <AvatarGroupCount>24</AvatarGroupCount>
</AvatarGroup>
```

### User Profile

```tsx
<div className="flex items-center gap-4">
  <Avatar size="lg">
    <AvatarImage
      src="https://i.imgur.com/xIe7Wlb.png"
      alt="@dwr.eth"
    />
    <AvatarFallback>DW</AvatarFallback>
    <AvatarBadge>
      <CheckIcon />
    </AvatarBadge>
  </Avatar>
  <div>
    <p className="font-semibold">Dan Romero</p>
    <p className="text-muted-foreground text-sm">@dwr.eth</p>
  </div>
</div>
```

### Activity Feed

```tsx
<div className="flex items-start gap-3">
  <Avatar>
    <AvatarImage src="/user.jpg" alt="@username" />
    <AvatarFallback>UN</AvatarFallback>
    <AvatarBadge className="bg-green-500">
      <ZapIcon />
    </AvatarBadge>
  </Avatar>
  <div className="flex-1">
    <p className="text-sm">
      <span className="font-medium">@username</span> cast a new post
    </p>
    <p className="text-muted-foreground text-xs">2 minutes ago</p>
  </div>
</div>
```

## Accessibility

- AvatarImage includes `alt` attribute for screen readers
- AvatarFallback provides text alternative when images fail to load
- Proper semantic HTML with appropriate ARIA attributes via Base UI
- Badge icons should be decorative; status communicated through other means

## Technical Notes

- Image loading handled automatically by Base UI Avatar primitive
- Fallback displays only when image fails to load or src is missing
- Badge automatically scales with avatar size (sm shows no icon, default/lg show icons)
- AvatarGroup applies negative spacing (`-space-x-2`) and ring borders automatically
- Uses `data-size` attribute for responsive badge sizing
- Group uses `/avatar` and `/avatar-group` context for nested styling

## Related

- [Button](./button.llm.md) - For clickable avatar actions
- [Card](./card.llm.md) - For profile cards with avatars
- [Dialog](./dialog.llm.md) - For profile modals

---

# Badge

Small label for displaying metadata, status indicators, counts, and categorical information.

## Import

```tsx
import { Badge } from "@neynar/ui/badge"
```

## Anatomy

```tsx
<Badge variant="default">Label</Badge>
```

## Props

### Badge

Extends all HTML span attributes plus:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "default" \| "secondary" \| "destructive" \| "success" \| "warning" \| "info" \| "outline" \| "ghost" \| "link" | "default" | Visual style variant |
| render | React.ReactElement \| ((state) => React.ReactElement) | - | Custom element to render as (e.g., `<a />`) |
| className | string | - | Additional CSS classes |

All standard HTML span attributes are supported (onClick, aria-*, data-*, etc.).

### Special Props

**render prop** - Render Badge as a different element:

```tsx
<Badge render={<a href="/docs" />}>Documentation</Badge>
```

## Variants

### Visual Variants

| Variant | Use Case | Appearance |
|---------|----------|------------|
| default | Primary emphasis, featured items | Solid primary color background |
| secondary | Neutral status, less emphasis | Subtle secondary background |
| outline | Low emphasis, borders | Border with transparent background |
| ghost | Minimal, hover reveals | Transparent, hover shows background |
| link | Clickable, link-style | Underlined text style |

### Semantic Variants

| Variant | Use Case | Appearance |
|---------|----------|------------|
| destructive | Errors, critical warnings | Red/destructive color with subtle background |
| success | Success states, confirmations | Green/success color with subtle background |
| warning | Warnings, alerts | Yellow/warning color with subtle background |
| info | Informational messages | Blue/info color with subtle background |

## Examples

### Basic Variants

```tsx
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="destructive">Error</Badge>
```

### With Icons

Add `data-icon="inline-start"` or `data-icon="inline-end"` to icon elements for proper spacing:

```tsx
import { StarIcon, FlameIcon } from "lucide-react"

<Badge>
  <StarIcon data-icon="inline-start" />
  Featured
</Badge>

<Badge variant="destructive">
  <FlameIcon data-icon="inline-start" />
  Hot
</Badge>
```

### Counter Badges

```tsx
<Badge>3</Badge>
<Badge>12</Badge>
<Badge>99+</Badge>
<Badge variant="destructive">5</Badge>
```

### Status Indicators

Use colored dots for operational status:

```tsx
<Badge variant="secondary">
  <span className="bg-green-500 mr-1 size-2 rounded-full" />
  Online
</Badge>

<Badge variant="secondary">
  <span className="bg-yellow-500 mr-1 size-2 rounded-full" />
  Degraded
</Badge>

<Badge variant="secondary">
  <span className="bg-red-500 mr-1 size-2 rounded-full" />
  Offline
</Badge>
```

### As Link

```tsx
<Badge render={<a href="/pricing" />} variant="outline">
  Upgrade to Pro
</Badge>
```

### Plan Tiers

```tsx
import { CrownIcon, SparklesIcon } from "lucide-react"

<Badge variant="secondary">Free</Badge>
<Badge>
  <SparklesIcon data-icon="inline-start" />
  Popular
</Badge>
<Badge variant="outline">
  <CrownIcon data-icon="inline-start" />
  Enterprise
</Badge>
```

## Data Attributes

The Badge component sets these data attributes for styling:

| Attribute | Value |
|-----------|-------|
| data-slot | "badge" |
| data-icon | "inline-start" or "inline-end" (on icon elements) |

## Styling

### Icon Sizing

Icons inside Badge automatically get `size-3` (12px). Use `data-icon="inline-start"` or `data-icon="inline-end"` for proper padding:

```tsx
// Automatic icon sizing and spacing
<Badge>
  <StarIcon data-icon="inline-start" />
  Text
</Badge>
```

### Custom Styling

```tsx
<Badge className="text-lg px-4">
  Large Badge
</Badge>
```

## Accessibility

- Badge uses semantic HTML `<span>` by default
- Can be rendered as `<a>` via render prop for clickable badges
- Supports all ARIA attributes for enhanced semantics
- No built-in focus management (use render prop with `<button>` or `<a>` for interactive badges)

## Related

- **Button** - For clickable actions instead of labels
- **Avatar** - Can be combined with Badge for status indicators
- **Card** - Badges commonly used in card headers for categorization

---

# Blockquote

Quoted content with left border accent and italic styling.

## Import

```tsx
import { Blockquote } from "@neynar/ui/typography"
```

## Usage

```tsx
<Blockquote>
  The best way to predict the future is to invent it.
</Blockquote>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| color | "default" \| "muted" \| "subtle" | "default" | Text color (limited to neutral colors) |
| className | string | - | Additional CSS classes |

## Examples

### Basic Quote

```tsx
<Blockquote>
  Design is not just what it looks like and feels like.
  Design is how it works.
</Blockquote>
```

### With Attribution

```tsx
<figure>
  <Blockquote>
    The only way to do great work is to love what you do.
  </Blockquote>
  <figcaption>
    <Caption>— Steve Jobs</Caption>
  </figcaption>
</figure>
```

### Muted Style

```tsx
<Blockquote color="muted">
  A subtle quote that doesn't demand attention.
</Blockquote>
```

### In Article Context

```tsx
<article>
  <Title order={2}>Key Insights</Title>
  <Text>The author makes several important points:</Text>
  <Blockquote>
    Simplicity is the ultimate sophistication.
  </Blockquote>
  <Text>This philosophy guides the entire design system.</Text>
</article>
```

## Styling

Default styling:
- Left border accent (`border-l-4 border-border`)
- Left padding (`pl-4`)
- Italic text

## Color Restriction

Colors are limited to `default`, `muted`, and `subtle` for semantic appropriateness. Blockquotes represent quoted content, not status messages.

## Related

- [Text](./text.llm.md) - Paragraph text
- [Title](./title.llm.md) - Headings
- [Card](./card.llm.md) - For featured quotes

---

# Breadcrumb

Semantic navigation component displaying hierarchical page location with accessible breadcrumb trail.

## Import

```tsx
import {
  Breadcrumb,
  BreadcrumbList,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbPage,
  BreadcrumbSeparator,
  BreadcrumbEllipsis,
} from "@neynar/ui/breadcrumb"
```

## Anatomy

```tsx
<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink href="/">Home</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbLink href="/dashboard">Dashboard</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>Settings</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>
```

## Components

| Component | Description |
|-----------|-------------|
| Breadcrumb | Root `<nav>` container with `aria-label="breadcrumb"` |
| BreadcrumbList | Ordered list (`<ol>`) with responsive flex layout |
| BreadcrumbItem | List item (`<li>`) wrapper for links/pages |
| BreadcrumbLink | Interactive link (`<a>`) for navigable pages, supports custom `render` prop |
| BreadcrumbPage | Current page indicator (`<span>`) with `aria-current="page"` |
| BreadcrumbSeparator | Visual separator (`<li>`) between items, defaults to chevron icon |
| BreadcrumbEllipsis | Collapsed section indicator (`<span>`) for long paths |

## Props

### Breadcrumb

Standard `<nav>` HTML attributes. Automatically includes `aria-label="breadcrumb"`.

### BreadcrumbList

Standard `<ol>` HTML attributes. Automatically styled with responsive spacing (1.5 gap on mobile, 2.5 on desktop).

### BreadcrumbItem

Standard `<li>` HTML attributes. Styled as inline-flex container.

### BreadcrumbLink

Extends standard `<a>` HTML attributes with Base UI's `render` prop support.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| render | ReactElement | - | Custom element to render as (e.g., Next.js Link) |
| href | string | - | Link destination |

**Using render prop:**
```tsx
import { Link } from "next/link"

<BreadcrumbLink render={<Link href="/dashboard" />}>
  Dashboard
</BreadcrumbLink>
```

### BreadcrumbPage

Standard `<span>` HTML attributes. Automatically includes `aria-current="page"` and `aria-disabled="true"`.

### BreadcrumbSeparator

Standard `<li>` HTML attributes with custom separator support.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | ChevronRightIcon | Custom separator content (icon or text) |

### BreadcrumbEllipsis

Standard `<span>` HTML attributes. Renders MoreHorizontalIcon with screen reader text "More".

## Data Attributes

All components include `data-slot` attributes for styling:

| Component | Attribute |
|-----------|-----------|
| Breadcrumb | `data-slot="breadcrumb"` |
| BreadcrumbList | `data-slot="breadcrumb-list"` |
| BreadcrumbItem | `data-slot="breadcrumb-item"` |
| BreadcrumbLink | `data-slot="breadcrumb-link"` |
| BreadcrumbPage | `data-slot="breadcrumb-page"` |
| BreadcrumbSeparator | `data-slot="breadcrumb-separator"` |
| BreadcrumbEllipsis | `data-slot="breadcrumb-ellipsis"` |

## Examples

### Basic Navigation

```tsx
<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink href="/">Home</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbLink href="/dashboard">Dashboard</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>Settings</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>
```

### With Icons

```tsx
import { HomeIcon, SettingsIcon } from "lucide-react"

<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink href="/" aria-label="Home">
        <HomeIcon className="size-4" />
      </BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>
        <SettingsIcon className="mr-2 size-4" />
        Settings
      </BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>
```

### Custom Separator

```tsx
import { SlashIcon } from "lucide-react"

<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink href="/">Home</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator>
      <SlashIcon />
    </BreadcrumbSeparator>
    <BreadcrumbItem>
      <BreadcrumbPage>Dashboard</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>
```

### Truncated Long Paths

```tsx
<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink href="/">Home</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbLink href="/dashboard">Dashboard</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbEllipsis />
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbLink href="/settings">Settings</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>API Keys</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>
```

### With Next.js Link

```tsx
import { Link } from "next/link"

<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink render={<Link href="/" />}>
        Home
      </BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbLink render={<Link href="/dashboard" />}>
        Dashboard
      </BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>Settings</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>
```

## Accessibility

- Root component uses semantic `<nav>` with `aria-label="breadcrumb"`
- Uses ordered list (`<ol>`) for proper navigation structure
- Current page has `aria-current="page"` and `aria-disabled="true"`
- Separators marked with `role="presentation"` and `aria-hidden="true"`
- Ellipsis includes screen reader text "More"
- Links have hover state with color transition for visual feedback
- Automatically wraps on small screens for responsive behavior

## Related

- [Link](/docs/components/link) - For custom link rendering
- [Navigation Menu](/docs/components/navigation-menu) - For primary navigation

---

# ButtonGroup

Groups related buttons together with fused borders and shared styling for toolbars, split buttons, and bulk actions.

## Import

```tsx
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "@neynar/ui/button-group"
```

## Anatomy

```tsx
<ButtonGroup orientation="horizontal">
  <Button>First</Button>
  <ButtonGroupSeparator />
  <Button>Second</Button>
  <ButtonGroupText>Status</ButtonGroupText>
</ButtonGroup>
```

## Components

| Component | Description |
|-----------|-------------|
| ButtonGroup | Root container with fused border styling |
| ButtonGroupSeparator | Visual divider between button sections |
| ButtonGroupText | Text/status display element within group |

## Props

### ButtonGroup

Extends all native `div` props plus:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| orientation | "horizontal" \| "vertical" | "horizontal" | Layout direction of buttons |

**Auto-behaviors:**
- Fuses borders between adjacent buttons
- Rounds only outer corners
- Handles focus visibility (z-index management)
- Supports Input and Select components

### ButtonGroupSeparator

Extends `Separator` component props:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| orientation | "horizontal" \| "vertical" | "vertical" | Separator direction |

**Note:** Typically inherits orientation from parent ButtonGroup context, but can be overridden.

### ButtonGroupText

Extends all native `div` props plus:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| render | React.Element \| function | - | Custom element or render function |

**Render prop example:**

```tsx
<ButtonGroupText render={<span />}>
  Custom text
</ButtonGroupText>
```

## Data Attributes

| Attribute | Element | Description |
|-----------|---------|-------------|
| data-slot="button-group" | ButtonGroup | Identifies container |
| data-slot="button-group-text" | ButtonGroupText | Identifies text element |
| data-slot="button-group-separator" | ButtonGroupSeparator | Identifies separator |
| data-orientation | ButtonGroup | Current orientation value |

## Examples

### Basic Horizontal Group

```tsx
<ButtonGroup>
  <Button variant="outline">Left</Button>
  <Button variant="outline">Center</Button>
  <Button variant="outline">Right</Button>
</ButtonGroup>
```

### Vertical Group with Icons

```tsx
<ButtonGroup orientation="vertical">
  <Button variant="secondary">
    <KeyIcon data-icon="inline-start" />
    API Keys
  </Button>
  <Button variant="secondary">
    <MailIcon data-icon="inline-start" />
    Webhooks
  </Button>
  <Button variant="secondary">
    <SettingsIcon data-icon="inline-start" />
    Settings
  </Button>
</ButtonGroup>
```

### Split Button Pattern

```tsx
<ButtonGroup>
  <Button variant="default">Save Changes</Button>
  <Button variant="default" size="icon">
    <ChevronDownIcon />
  </Button>
</ButtonGroup>
```

### With Separators

```tsx
<ButtonGroup>
  <Button variant="outline" size="sm">
    <CopyIcon data-icon="inline-start" />
    Copy
  </Button>
  <Button variant="outline" size="sm">
    <EditIcon data-icon="inline-start" />
    Edit
  </Button>
  <ButtonGroupSeparator />
  <Button variant="outline" size="sm">
    <TrashIcon data-icon="inline-start" />
    Delete
  </Button>
</ButtonGroup>
```

### Pagination with Text

```tsx
<ButtonGroup>
  <Button variant="outline" size="sm" disabled>
    Previous
  </Button>
  <ButtonGroupText>1 of 10</ButtonGroupText>
  <Button variant="outline" size="sm">
    Next
  </Button>
</ButtonGroup>
```

### Bulk Actions with Status

```tsx
<ButtonGroup>
  <ButtonGroupText>
    <CheckIcon />
    3 selected
  </ButtonGroupText>
  <Button variant="outline" size="sm">
    <RefreshCwIcon data-icon="inline-start" />
    Regenerate
  </Button>
  <Button variant="outline" size="sm">
    <DownloadIcon data-icon="inline-start" />
    Export
  </Button>
  <ButtonGroupSeparator />
  <Button variant="outline" size="sm">
    <TrashIcon data-icon="inline-start" />
    Delete
  </Button>
</ButtonGroup>
```

### Search Bar with Input

```tsx
<ButtonGroup>
  <Input placeholder="Search..." className="flex-1" />
  <Button variant="outline" size="icon">
    <SearchIcon />
  </Button>
</ButtonGroup>
```

### Filter Toggle Group

```tsx
function FilterExample() {
  const [filter, setFilter] = useState("all")

  return (
    <ButtonGroup>
      <Button
        variant={filter === "all" ? "secondary" : "outline"}
        size="sm"
        onClick={() => setFilter("all")}
      >
        All
      </Button>
      <Button
        variant={filter === "active" ? "secondary" : "outline"}
        size="sm"
        onClick={() => setFilter("active")}
      >
        Active
      </Button>
      <Button
        variant={filter === "inactive" ? "secondary" : "outline"}
        size="sm"
        onClick={() => setFilter("inactive")}
      >
        Inactive
      </Button>
    </ButtonGroup>
  )
}
```

## Keyboard

ButtonGroup inherits keyboard navigation from child components:

| Key | Action |
|-----|--------|
| Tab | Navigate to next focusable element |
| Shift+Tab | Navigate to previous focusable element |
| Space/Enter | Activate focused button |

## Accessibility

- Uses `role="group"` to semantically group related buttons
- Maintains proper focus order through natural DOM structure
- Focus visibility handled with z-index elevation on `:focus-visible`
- Child buttons maintain individual ARIA attributes and keyboard support

## Related

- [Button](./button.llm.md) - Individual button component
- [Separator](./separator.llm.md) - Standalone separator
- [Input](./input.llm.md) - Works with Input in search patterns
- [Select](./select.llm.md) - Works with Select in filter patterns

---

# Button

Versatile button component with multiple visual variants, semantic colors, and flexible sizing.

## Import

```tsx
import { Button } from "@neynar/ui/button"
```

## Anatomy

```tsx
<Button variant="default" size="default">
  Click me
</Button>
```

## Props

### Button

Extends all standard HTML button attributes plus Base UI Button props.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "default" \| "outline" \| "secondary" \| "ghost" \| "destructive" \| "success" \| "warning" \| "info" \| "link" | "default" | Visual style variant |
| size | "default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" | "default" | Button size |
| disabled | boolean | false | Disables the button |
| render | ReactElement \| Function | - | Custom element to render as (maintains button semantics) |
| className | string | - | Additional CSS classes |
| focusableWhenDisabled | boolean | true | Whether button remains focusable when disabled |
| nativeButton | boolean | true | Whether component renders native `<button>` when using render prop |

All standard `<button>` HTML attributes are supported: `type`, `onClick`, `onFocus`, `aria-label`, etc.

### Icon Placement

Icons are placed using `data-icon` attributes on child elements:

```tsx
// Icon at start
<Button>
  <PlusIcon data-icon="inline-start" />
  Create
</Button>

// Icon at end
<Button>
  Continue
  <ArrowRightIcon data-icon="inline-end" />
</Button>
```

The component automatically sizes icons to `size-4` (16px) except for `xs` sizes which use `size-3` (12px). Icons receive proper spacing via gap utilities.

### Render Prop

Use the `render` prop to compose the button with other elements while maintaining button semantics:

```tsx
// Render as link
<Button render={<a href="/dashboard" />}>
  Dashboard
</Button>

// Compose with Next.js Link
<Button render={<Link href="/login" />}>
  Sign in
</Button>
```

## Variants

### Visual Variants

| Variant | Use Case |
|---------|----------|
| default | Primary actions, most important button in a context |
| outline | Secondary actions, neutral emphasis with border |
| secondary | Alternative actions, less emphasis than default |
| ghost | Tertiary actions, minimal visual weight |
| link | Text-only actions that look like links |

### Semantic Variants

| Variant | Use Case |
|---------|----------|
| destructive | Delete, revoke, or other destructive actions |
| success | Confirm, approve, or positive actions |
| warning | Caution, proceed with care actions |
| info | Informational or help actions |

### Size Variants

| Size | Height | Use Case |
|------|--------|----------|
| xs | 24px (h-6) | Compact UIs, inline actions, tags |
| sm | 32px (h-8) | Dense tables, toolbars, secondary actions |
| default | 36px (h-9) | Standard buttons in forms and dialogs |
| lg | 40px (h-10) | Marketing pages, prominent CTAs |

### Icon-Only Sizes

| Size | Dimensions | Use Case |
|------|------------|----------|
| icon-xs | 24x24px | Compact icon buttons |
| icon-sm | 32x32px | Small icon buttons in toolbars |
| icon | 36x36px | Standard icon buttons |
| icon-lg | 40x40px | Large icon buttons for emphasis |

## Data Attributes

| Attribute | When Present |
|-----------|--------------|
| data-slot="button" | Always present (for styling context) |
| data-disabled | Button is disabled |
| data-focusable | Button remains focusable when disabled |
| aria-expanded | Button controls expandable content (auto-styled) |
| aria-invalid | Button is in invalid state (auto-styled with destructive ring) |

## Examples

### Basic Usage

```tsx
<Button>Click me</Button>
<Button variant="outline">Cancel</Button>
<Button variant="destructive">Delete</Button>
```

### With Icons

```tsx
// Icon at start
<Button>
  <PlusIcon data-icon="inline-start" />
  Create Key
</Button>

// Icon at end
<Button variant="secondary">
  Continue
  <SendIcon data-icon="inline-end" />
</Button>

// Icon only - requires aria-label
<Button size="icon" variant="ghost" aria-label="Settings">
  <SettingsIcon />
</Button>
```

### Loading State

```tsx
function SubmitButton() {
  const [isLoading, setIsLoading] = useState(false)

  return (
    <Button disabled={isLoading}>
      {isLoading ? (
        <Loader2Icon data-icon="inline-start" className="animate-spin" />
      ) : (
        <SendIcon data-icon="inline-start" />
      )}
      {isLoading ? "Sending..." : "Send"}
    </Button>
  )
}
```

### API Key Management Context

```tsx
function APIKeyActions() {
  const [copied, setCopied] = useState(false)

  return (
    <div className="flex gap-2">
      <Button variant="outline" size="sm" onClick={() => setCopied(true)}>
        <CopyIcon data-icon="inline-start" />
        {copied ? "Copied!" : "Copy"}
      </Button>
      <Button variant="outline" size="sm">
        <RefreshCwIcon data-icon="inline-start" />
        Regenerate
      </Button>
      <Button variant="destructive" size="sm">
        <TrashIcon data-icon="inline-start" />
        Revoke
      </Button>
    </div>
  )
}
```

### Form Actions

```tsx
<div className="flex justify-end gap-2">
  <Button variant="outline" type="button">Cancel</Button>
  <Button type="submit">
    <SendIcon data-icon="inline-start" />
    Save Changes
  </Button>
</div>
```

## Keyboard

| Key | Action |
|-----|--------|
| Space | Activate button |
| Enter | Activate button |
| Tab | Move focus to/from button |

## Accessibility

- Rendered as native `<button>` element by default
- Supports `aria-label` for icon-only buttons (required for accessibility)
- Disabled buttons remain focusable by default (set `focusableWhenDisabled={false}` to change)
- Focus visible ring with 3px width for clear focus indication
- Supports `aria-invalid` state for form validation with red ring/border
- Screen readers announce button role and state automatically

## Styling Notes

### Icon Auto-Sizing

Icons are automatically sized based on button size:
- Default sizes: 16px (`size-4`)
- `xs` and `icon-xs`: 12px (`size-3`)
- Override with explicit class: `<Icon className="size-5" />`

### Button Group Context

When inside `data-slot="button-group"`, buttons automatically adjust border radius to connect seamlessly.

### Dark Mode

All variants include dark mode optimized colors that automatically adapt based on theme settings.

## Related

- **Button Group** - Group multiple buttons with connected borders
- **Toggle Button** - Button with on/off state
- **Dialog** - Use Button as DialogTrigger

---

# Calendar

Date picker component supporting single date, date range, and multiple date selection modes with customizable appearance and navigation.

## Import

```tsx
import { Calendar } from "@neynar/ui/calendar"
```

## Anatomy

```tsx
<Calendar
  mode="single"
  selected={date}
  onSelect={setDate}
/>
```

## Props

### Calendar

Built on react-day-picker's DayPicker component. All DayPicker props are supported.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| mode | "single" \| "multiple" \| "range" | - | Selection mode for dates |
| selected | Date \| Date[] \| { from: Date; to?: Date } | - | Currently selected date(s) |
| onSelect | (date) => void | - | Called when selection changes |
| buttonVariant | "default" \| "outline" \| "secondary" \| "ghost" \| "destructive" \| "link" | "ghost" | Visual style for navigation buttons |
| showOutsideDays | boolean | true | Show days from adjacent months |
| captionLayout | "label" \| "dropdown" \| "dropdown-months" \| "dropdown-years" | "label" | Layout style for month/year header |
| numberOfMonths | number | 1 | Number of months to display |
| disabled | (date: Date) => boolean \| boolean | - | Disable specific dates or all dates |
| fromDate | Date | - | Earliest selectable date |
| toDate | Date | - | Latest selectable date |
| fromYear | number | - | Earliest year for dropdown navigation |
| toYear | number | - | Latest year for dropdown navigation |
| showWeekNumber | boolean | false | Display ISO week numbers |
| formatters | Partial&lt;Formatters&gt; | - | Override default date formatting |
| footer | ReactNode | - | Content for calendar footer |
| className | string | - | Additional CSS classes |
| classNames | Record&lt;string, string&gt; | - | Class names for internal elements |

### Selection Modes

**Single Mode** - Select one date:
```tsx
const [date, setDate] = useState<Date | undefined>()
<Calendar mode="single" selected={date} onSelect={setDate} />
```

**Range Mode** - Select a date range:
```tsx
const [range, setRange] = useState<{ from: Date; to?: Date }>()
<Calendar
  mode="range"
  selected={range}
  onSelect={(range) => range?.from && setRange(range)}
/>
```

**Multiple Mode** - Select multiple dates:
```tsx
const [dates, setDates] = useState<Date[]>([])
<Calendar mode="multiple" selected={dates} onSelect={setDates} />
```

## Data Attributes

| Attribute | When Present | Applied To |
|-----------|--------------|------------|
| data-slot | Always "calendar" | Root element |
| data-selected-single | Single date selected (not in range) | Day button |
| data-range-start | First date in range selection | Day button |
| data-range-middle | Date within range | Day button |
| data-range-end | Last date in range selection | Day button |
| data-day | Always (date string) | Day button |

## Examples

### Basic Single Date Selection

```tsx
function DatePicker() {
  const [date, setDate] = useState<Date | undefined>(new Date())

  return (
    <Calendar
      mode="single"
      selected={date}
      onSelect={setDate}
    />
  )
}
```

### Date Range with Two Months

```tsx
function DateRangePicker() {
  const [dateRange, setDateRange] = useState<{ from: Date; to?: Date }>({
    from: subDays(new Date(), 7),
    to: new Date(),
  })

  return (
    <Calendar
      mode="range"
      selected={dateRange}
      onSelect={(range) => range?.from && setDateRange(range)}
      numberOfMonths={2}
    />
  )
}
```

### With Disabled Dates

```tsx
function RestrictedCalendar() {
  const [date, setDate] = useState<Date | undefined>()

  return (
    <Calendar
      mode="single"
      selected={date}
      onSelect={setDate}
      disabled={(date) => date < new Date() || date > addDays(new Date(), 30)}
    />
  )
}
```

### Dropdown Navigation with Year Range

```tsx
function CalendarWithDropdown() {
  const [date, setDate] = useState<Date | undefined>()

  return (
    <Calendar
      mode="single"
      selected={date}
      onSelect={setDate}
      captionLayout="dropdown"
      fromYear={2020}
      toYear={2030}
    />
  )
}
```

### With Week Numbers

```tsx
function CalendarWithWeeks() {
  const [date, setDate] = useState<Date | undefined>()

  return (
    <Calendar
      mode="single"
      selected={date}
      onSelect={setDate}
      showWeekNumber
    />
  )
}
```

### Multiple Date Selection

```tsx
function MultiDatePicker() {
  const [dates, setDates] = useState<Date[]>([
    new Date(),
    addDays(new Date(), 2),
    addDays(new Date(), 5),
  ])

  return (
    <Calendar
      mode="multiple"
      selected={dates}
      onSelect={(dates) => dates && setDates(dates)}
    />
  )
}
```

### Custom Button Variant

```tsx
function StyledCalendar() {
  const [date, setDate] = useState<Date | undefined>()

  return (
    <Calendar
      mode="single"
      selected={date}
      onSelect={setDate}
      buttonVariant="outline"
    />
  )
}
```

### Hide Outside Days

```tsx
function CompactCalendar() {
  const [date, setDate] = useState<Date | undefined>()

  return (
    <Calendar
      mode="single"
      selected={date}
      onSelect={setDate}
      showOutsideDays={false}
    />
  )
}
```

## Keyboard

| Key | Action |
|-----|--------|
| Arrow Keys | Navigate between days |
| Enter / Space | Select focused day |
| Page Up | Previous month |
| Page Down | Next month |
| Home | First day of week |
| End | Last day of week |
| Shift + Page Up | Previous year |
| Shift + Page Down | Next year |

## Accessibility

- Full keyboard navigation with arrow keys, Page Up/Down, and modifier keys
- ARIA labels for all navigation buttons and date selections
- Focus management with visual focus indicators
- Screen reader announcements for date selection changes
- Respects reduced-motion preferences

## Related

- [Button](./button.llm.md) - Used for navigation controls
- [Popover](./popover.llm.md) - Often used to contain calendar in dropdown
- [Card](./card.llm.md) - Can contain calendar for date range selection UI

---

# Card

Flexible card container with compound components for building structured layouts with headers, content, actions, and footers.

## Import

```tsx
import {
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardAction,
  CardContent,
  CardFooter,
} from "@neynar/ui/card"
```

## Anatomy

```tsx
<Card>
  <CardHeader>
    <CardTitle />
    <CardDescription />
    <CardAction />
  </CardHeader>
  <CardContent />
  <CardFooter />
</Card>
```

## Components

| Component | Description |
|-----------|-------------|
| Card | Root container with shadow, border, rounded corners, and size variants |
| CardHeader | Grid layout header supporting title, description, and action button |
| CardTitle | Heading or name displayed in the header |
| CardDescription | Secondary text below the title with muted styling |
| CardAction | Container for buttons/controls positioned in top-right of header |
| CardContent | Main content area with consistent padding |
| CardFooter | Footer section for actions or metadata |

## Props

### Card

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| size | "default" \| "sm" | "default" | Controls padding and spacing (default: py-6 px-6 gap-6, sm: py-4 px-4 gap-4) |
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Card content |

**Standard div props**: All `React.ComponentProps<"div">` are supported.

### CardHeader

Automatically creates a grid layout:
- **Without CardAction**: Single column for title/description
- **With CardAction**: Two columns `[1fr auto]` with action in top-right
- **With CardDescription**: Two rows for title and description

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Header content (Title, Description, Action) |

**Standard div props**: All `React.ComponentProps<"div">` are supported.

### CardTitle

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Title text or content |

**Standard div props**: All `React.ComponentProps<"div">` are supported.

**Default styling**: `text-base font-medium` (default size), `text-sm font-medium` (small size)

### CardDescription

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Description text |

**Standard div props**: All `React.ComponentProps<"div">` are supported.

**Default styling**: `text-sm text-muted-foreground`

### CardAction

Automatically positioned in the top-right corner of the header, spanning both title and description rows.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Action buttons or controls |

**Standard div props**: All `React.ComponentProps<"div">` are supported.

### CardContent

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Main card content |

**Standard div props**: All `React.ComponentProps<"div">` are supported.

### CardFooter

Add `border-t` class for visual separation from content.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Footer content |

**Standard div props**: All `React.ComponentProps<"div">` are supported.

**Default styling**: `flex items-center`

## Data Attributes

### Card

| Attribute | Value | When Applied |
|-----------|-------|--------------|
| data-slot | "card" | Always |
| data-size | "default" \| "sm" | Always (matches size prop) |

### Subcomponents

All subcomponents include `data-slot` attributes for styling:
- `data-slot="card-header"`
- `data-slot="card-title"`
- `data-slot="card-description"`
- `data-slot="card-action"`
- `data-slot="card-content"`
- `data-slot="card-footer"`

## Variants

### Size

| Variant | Padding | Gap | Font Size (Title) |
|---------|---------|-----|-------------------|
| default | py-6 px-6 | gap-6 | text-base |
| sm | py-4 px-4 | gap-4 | text-sm |

## Examples

### Basic Card

```tsx
<Card>
  <CardHeader>
    <CardTitle>API Usage</CardTitle>
    <CardDescription>Your current billing period</CardDescription>
  </CardHeader>
  <CardContent>
    <p>847,231 / 1,000,000 requests used</p>
  </CardContent>
</Card>
```

### With Action Button

```tsx
<Card>
  <CardHeader>
    <CardTitle>Settings</CardTitle>
    <CardDescription>Manage your preferences</CardDescription>
    <CardAction>
      <Button variant="outline">Edit</Button>
    </CardAction>
  </CardHeader>
  <CardContent>
    <p>Your account settings and preferences.</p>
  </CardContent>
</Card>
```

### With Footer Actions

```tsx
<Card>
  <CardHeader>
    <CardTitle>Confirm Action</CardTitle>
    <CardDescription>This action cannot be undone</CardDescription>
  </CardHeader>
  <CardContent>
    <p>Are you sure you want to delete this item?</p>
  </CardContent>
  <CardFooter className="border-t">
    <Button variant="outline">Cancel</Button>
    <Button variant="destructive">Delete</Button>
  </CardFooter>
</Card>
```

### Small Size Card

```tsx
<Card size="sm">
  <CardHeader>
    <CardTitle>Compact Card</CardTitle>
    <CardDescription>Reduced padding</CardDescription>
  </CardHeader>
  <CardContent>
    <p>Small size for dense layouts.</p>
  </CardContent>
</Card>
```

### Content Only

```tsx
<Card>
  <CardContent>
    <p>Simple card with just content, no header or footer.</p>
  </CardContent>
</Card>
```

### With Status Indicator

```tsx
<Card>
  <CardHeader>
    <div className="flex items-center gap-2">
      <CheckCircle2Icon className="text-green-500 size-5" />
      <CardTitle>Success</CardTitle>
    </div>
    <CardDescription>Operation completed successfully</CardDescription>
  </CardHeader>
  <CardContent>
    <p>Your changes have been saved.</p>
  </CardContent>
</Card>
```

### Dashboard Stats Card

```tsx
<Card>
  <CardHeader>
    <CardTitle>Total Requests</CardTitle>
    <CardDescription>Last 30 days</CardDescription>
  </CardHeader>
  <CardContent>
    <div className="space-y-2">
      <div className="text-3xl font-bold">1,284,563</div>
      <div className="text-muted-foreground flex items-center gap-1 text-sm">
        <TrendingUpIcon className="text-green-500 size-4" />
        <span className="text-green-500">12.5%</span> from last month
      </div>
    </div>
  </CardContent>
</Card>
```

### With Progress and Footer

```tsx
<Card>
  <CardHeader>
    <CardTitle>API Rate Limit</CardTitle>
    <CardDescription>Requests this month: 847,231 / 1,000,000</CardDescription>
    <CardAction>
      <Button variant="outline">Upgrade</Button>
    </CardAction>
  </CardHeader>
  <CardContent>
    <div className="space-y-2">
      <Progress value={84.7} className="h-2" />
      <div className="flex justify-between text-sm">
        <span className="text-muted-foreground">84.7% used</span>
        <span className="text-muted-foreground">152,769 remaining</span>
      </div>
    </div>
  </CardContent>
  <CardFooter className="border-t">
    <p className="text-muted-foreground text-sm">
      Resets on January 1, 2024
    </p>
  </CardFooter>
</Card>
```

### Complex Layout with Avatar

```tsx
<Card>
  <CardHeader>
    <div className="flex items-start gap-4">
      <div className="bg-primary/10 text-primary flex size-12 items-center justify-center rounded-full text-lg font-semibold">
        JD
      </div>
      <div className="flex-1">
        <div className="flex items-center gap-2">
          <CardTitle>Jane Doe</CardTitle>
          <Badge variant="outline">Admin</Badge>
        </div>
        <CardDescription>jane.doe@example.com</CardDescription>
      </div>
    </div>
    <CardAction>
      <Button variant="ghost">Edit</Button>
    </CardAction>
  </CardHeader>
  <CardContent>
    <div className="space-y-3">
      <div className="flex items-center justify-between">
        <span className="text-sm">API Access</span>
        <CheckCircle2Icon className="text-green-500 size-5" />
      </div>
    </div>
  </CardContent>
  <CardFooter className="border-t">
    <span className="text-muted-foreground text-sm">
      Last active: 2 hours ago
    </span>
  </CardFooter>
</Card>
```

## Theming

The Card component uses the following CSS custom properties:

- `--card`: Background color
- `--card-foreground`: Text color
- `--border`: Ring/border color
- `--muted-foreground`: Description text color

For frosted glass effects, the component respects:
- `--surface-blur`: Backdrop blur amount
- 75% opacity on `--card` background

## Accessibility

- Use semantic HTML elements within cards for proper structure
- CardTitle should contain heading elements when appropriate (`<h2>`, `<h3>`, etc.)
- Ensure sufficient color contrast between text and card background
- Footer buttons should have clear, actionable labels

## Related

- [Button](./button.llm.md) - For actions in CardAction and CardFooter
- [Badge](./badge.llm.md) - For status indicators in headers
- [Progress](./progress.llm.md) - For usage stats within cards
- [Avatar](./avatar.llm.md) - For user cards with profile images

---

# Carousel

Embla-powered carousel component with horizontal/vertical scrolling, keyboard navigation, and programmatic control.

## Import

```tsx
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
  type CarouselApi,
} from "@neynar/ui/carousel"
```

## Anatomy

```tsx
<Carousel>
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>
```

## Components

| Component | Description |
|-----------|-------------|
| Carousel | Root container, manages state and keyboard navigation |
| CarouselContent | Scrollable container with overflow handling |
| CarouselItem | Individual slide wrapper |
| CarouselPrevious | Previous button (auto-disabled at start) |
| CarouselNext | Next button (auto-disabled at end) |

## Props

### Carousel

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| orientation | "horizontal" \| "vertical" | "horizontal" | Scroll direction |
| opts | EmblaOptionsType | - | Embla carousel options (loop, align, etc.) |
| plugins | EmblaPluginType[] | - | Embla carousel plugins |
| setApi | (api: CarouselApi) => void | - | Callback to receive carousel API for programmatic control |

### CarouselContent

Standard div props. Automatically applies flex layout and orientation-based spacing.

### CarouselItem

Standard div props. Use Tailwind `basis-*` classes to control items per view:
- `basis-full` - 1 item per view (default)
- `md:basis-1/2` - 2 items on medium screens
- `lg:basis-1/3` - 3 items on large screens

### CarouselPrevious / CarouselNext

Extends Button props with defaults: `variant="outline"`, `size="icon-sm"`.

## Embla Options (opts prop)

Common options passed to `opts`:

| Option | Type | Description |
|--------|------|-------------|
| loop | boolean | Enable infinite looping |
| align | "start" \| "center" \| "end" | Slide alignment |
| slidesToScroll | number | Number of slides to scroll at once |
| skipSnaps | boolean | Skip snapping to slides that are out of view |

Full options: https://www.embla-carousel.com/api/options/

## Data Attributes

| Attribute | Element | Description |
|-----------|---------|-------------|
| data-slot="carousel" | Carousel root | For styling hooks |
| data-slot="carousel-content" | Content wrapper | For styling hooks |
| data-slot="carousel-item" | Individual item | For styling hooks |
| data-slot="carousel-previous" | Previous button | For styling hooks |
| data-slot="carousel-next" | Next button | For styling hooks |

## Examples

### Basic Usage

```tsx
<Carousel className="w-full max-w-sm">
  <CarouselContent>
    {items.map((item, index) => (
      <CarouselItem key={index}>
        <Card>
          <CardContent>{item.content}</CardContent>
        </Card>
      </CarouselItem>
    ))}
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>
```

### Multiple Items Per View

```tsx
<Carousel className="w-full">
  <CarouselContent>
    {items.map((item, index) => (
      <CarouselItem key={index} className="md:basis-1/2 lg:basis-1/3">
        <Card>
          <CardContent>{item.content}</CardContent>
        </Card>
      </CarouselItem>
    ))}
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>
```

### Loop Mode

```tsx
<Carousel
  opts={{
    loop: true,
    align: "start",
  }}
>
  <CarouselContent>
    {items.map((item, index) => (
      <CarouselItem key={index}>
        <Card>
          <CardContent>{item.content}</CardContent>
        </Card>
      </CarouselItem>
    ))}
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>
```

### Vertical Orientation

```tsx
<Carousel orientation="vertical" className="w-full max-w-xs">
  <CarouselContent className="h-[300px]">
    {items.map((item, index) => (
      <CarouselItem key={index}>
        <Card>
          <CardContent>{item.content}</CardContent>
        </Card>
      </CarouselItem>
    ))}
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>
```

### Programmatic Control with Custom Indicators

```tsx
function ControlledCarousel() {
  const [api, setApi] = useState<CarouselApi>()
  const [current, setCurrent] = useState(0)
  const [count, setCount] = useState(0)

  useEffect(() => {
    if (!api) return

    setCount(api.scrollSnapList().length)
    setCurrent(api.selectedScrollSnap())

    api.on("select", () => {
      setCurrent(api.selectedScrollSnap())
    })
  }, [api])

  return (
    <div className="space-y-4">
      <Carousel setApi={setApi} className="w-full max-w-sm">
        <CarouselContent>
          {items.map((item, index) => (
            <CarouselItem key={index}>
              <Card>
                <CardContent>{item.content}</CardContent>
              </Card>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
      </Carousel>

      {/* Custom indicators */}
      <div className="flex justify-center gap-2">
        {Array.from({ length: count }).map((_, index) => (
          <button
            key={index}
            className={cn(
              "size-2 rounded-full transition-all",
              index === current
                ? "bg-primary w-6"
                : "bg-muted-foreground/30 hover:bg-muted-foreground/50"
            )}
            onClick={() => api?.scrollTo(index)}
            aria-label={`Go to slide ${index + 1}`}
          />
        ))}
      </div>

      <p className="text-center text-sm text-muted-foreground">
        Slide {current + 1} of {count}
      </p>
    </div>
  )
}
```

## Keyboard

| Key | Action |
|-----|--------|
| ArrowLeft | Previous slide (horizontal) |
| ArrowRight | Next slide (horizontal) |

## Accessibility

- Root has `role="region"` and `aria-roledescription="carousel"`
- Items have `role="group"` and `aria-roledescription="slide"`
- Navigation buttons include screen reader text ("Previous slide", "Next slide")
- Buttons automatically disabled when navigation not available
- Full keyboard support with arrow key navigation

## useCarousel Hook

Access carousel context in child components:

```tsx
function CustomCarouselControl() {
  const { api, scrollPrev, scrollNext, canScrollPrev, canScrollNext } = useCarousel()

  return (
    <div>
      <Button onClick={scrollPrev} disabled={!canScrollPrev}>Prev</Button>
      <Button onClick={scrollNext} disabled={!canScrollNext}>Next</Button>
    </div>
  )
}
```

## CarouselApi Methods

Key methods available via `setApi` callback:

| Method | Description |
|--------|-------------|
| `scrollTo(index)` | Scroll to specific slide |
| `scrollPrev()` | Scroll to previous slide |
| `scrollNext()` | Scroll to next slide |
| `canScrollPrev()` | Check if can scroll backward |
| `canScrollNext()` | Check if can scroll forward |
| `selectedScrollSnap()` | Get current slide index |
| `scrollSnapList()` | Get array of all snap points |
| `on(event, callback)` | Subscribe to events (select, init, reInit) |

Full API: https://www.embla-carousel.com/api/

## Related

- Card - Common carousel item container
- Button - Used for navigation controls

---

# Chart

Flexible chart container built on Recharts with theming, configuration, and accessibility support for all chart types.

## Import

```tsx
import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ChartLegend,
  ChartLegendContent,
  type ChartConfig,
} from "@neynar/ui/chart"
```

## Anatomy

```tsx
<ChartContainer config={chartConfig}>
  <LineChart data={data}>
    <XAxis dataKey="name" />
    <YAxis />
    <ChartTooltip content={<ChartTooltipContent />} />
    <ChartLegend content={<ChartLegendContent />} />
    <Line dataKey="value" />
  </LineChart>
</ChartContainer>
```

## Components

| Component | Description |
|-----------|-------------|
| ChartContainer | Root container providing responsive layout, theme context, and CSS variables |
| ChartTooltip | Re-export of Recharts Tooltip for positioning (use with ChartTooltipContent) |
| ChartTooltipContent | Custom tooltip content with series indicators and formatting |
| ChartLegend | Re-export of Recharts Legend for positioning (use with ChartLegendContent) |
| ChartLegendContent | Custom legend content with icons and colors |

## Props

### ChartContainer

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| config | ChartConfig | - | Chart configuration defining series labels, colors, and icons |
| children | ReactNode | - | Recharts chart component (LineChart, BarChart, AreaChart, etc.) |
| className | string | - | Additional classes for sizing (e.g., "h-[400px]") |
| id | string | - | Optional unique identifier for the chart |

### ChartConfig

The config object maps series keys to their display configuration:

```tsx
const config = {
  revenue: {
    label: "Revenue",
    color: "hsl(142 76% 36%)", // Single color
  },
  expenses: {
    label: "Expenses",
    theme: { // Theme-aware colors
      light: "hsl(0 84% 60%)",
      dark: "hsl(0 72% 50%)",
    },
    icon: TrendingDown, // Optional icon component
  },
} satisfies ChartConfig
```

Each series can have:
- `label`: Display name for tooltips and legends
- `color`: Single color for both themes, OR
- `theme`: Object with `light` and `dark` colors
- `icon`: Optional React component for legend

### ChartTooltipContent

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| indicator | "dot" \| "line" \| "dashed" | "dot" | Visual indicator style for series |
| hideLabel | boolean | false | Hide the tooltip label (x-axis value) |
| hideIndicator | boolean | false | Hide series color indicators |
| nameKey | string | - | Override key for series names |
| labelKey | string | - | Override key for tooltip label |
| formatter | function | - | Custom value formatter |
| labelFormatter | function | - | Custom label formatter |

### ChartLegendContent

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| hideIcon | boolean | false | Hide series icons, showing only color squares |
| nameKey | string | - | Override key for series names |
| verticalAlign | "top" \| "bottom" | "bottom" | Legend position |

## Data Attributes

| Attribute | Applied To | Description |
|-----------|------------|-------------|
| data-slot | ChartContainer | Always "chart" |
| data-chart | ChartContainer | Unique chart ID for CSS variable scoping |

## Chart Types

The container works with all Recharts chart types:

- **LineChart**: Trends over time
- **BarChart**: Comparing values (vertical or horizontal)
- **AreaChart**: Filled regions showing volume
- **PieChart**: Proportional data
- **RadialBarChart**: Circular progress or gauges

## Examples

### Basic Line Chart

```tsx
import { Line, LineChart, XAxis, YAxis } from "recharts"

const data = [
  { month: "Jan", value: 2400 },
  { month: "Feb", value: 1398 },
  { month: "Mar", value: 9800 },
]

const config = {
  value: {
    label: "Revenue",
    color: "var(--chart-1)",
  },
} satisfies ChartConfig

<ChartContainer config={config} className="h-[300px]">
  <LineChart data={data}>
    <XAxis dataKey="month" />
    <YAxis />
    <ChartTooltip content={<ChartTooltipContent />} />
    <Line dataKey="value" stroke="var(--color-value)" strokeWidth={2} />
  </LineChart>
</ChartContainer>
```

### Multi-Series with Legend

```tsx
import { Area, AreaChart, XAxis, YAxis } from "recharts"

const data = [
  { month: "Jan", revenue: 12400, expenses: 8200 },
  { month: "Feb", revenue: 18200, expenses: 9100 },
]

const config = {
  revenue: { label: "Revenue", color: "hsl(142 76% 36%)" },
  expenses: { label: "Expenses", color: "hsl(0 84% 60%)" },
} satisfies ChartConfig

<ChartContainer config={config} className="h-[400px]">
  <AreaChart data={data}>
    <XAxis dataKey="month" />
    <YAxis />
    <ChartTooltip content={<ChartTooltipContent />} />
    <ChartLegend content={<ChartLegendContent />} />
    <Area
      dataKey="revenue"
      fill="var(--color-revenue)"
      stroke="var(--color-revenue)"
      fillOpacity={0.6}
    />
    <Area
      dataKey="expenses"
      fill="var(--color-expenses)"
      stroke="var(--color-expenses)"
      fillOpacity={0.4}
    />
  </AreaChart>
</ChartContainer>
```

### Bar Chart with Custom Tooltip

```tsx
import { Bar, BarChart, XAxis, YAxis } from "recharts"

const config = {
  requests: { label: "API Requests", color: "var(--chart-1)" },
} satisfies ChartConfig

<ChartContainer config={config} className="h-[300px]">
  <BarChart data={apiData}>
    <XAxis dataKey="endpoint" />
    <YAxis />
    <ChartTooltip
      content={
        <ChartTooltipContent
          formatter={(value) => [value.toLocaleString(), "Requests"]}
        />
      }
    />
    <Bar dataKey="requests" fill="var(--color-requests)" radius={4} />
  </BarChart>
</ChartContainer>
```

### Theme-Aware Colors

```tsx
const config = {
  active: {
    label: "Active Users",
    theme: {
      light: "hsl(220 70% 50%)",
      dark: "hsl(220 70% 60%)",
    },
  },
} satisfies ChartConfig

<ChartContainer config={config}>
  <LineChart data={data}>
    {/* Color automatically switches with theme */}
    <Line dataKey="active" stroke="var(--color-active)" />
  </LineChart>
</ChartContainer>
```

### Tooltip Indicators

```tsx
// Dot indicator (default)
<ChartTooltip content={<ChartTooltipContent indicator="dot" />} />

// Line indicator
<ChartTooltip content={<ChartTooltipContent indicator="line" />} />

// Dashed indicator
<ChartTooltip content={<ChartTooltipContent indicator="dashed" />} />
```

## Color Variables

ChartContainer generates CSS variables for each config key:

```tsx
const config = {
  revenue: { color: "hsl(142 76% 36%)" },
}
// Generates: --color-revenue: hsl(142 76% 36%)
// Use in charts: var(--color-revenue)
```

Recharts built-in chart colors are also available:
- `var(--chart-1)` through `var(--chart-5)`: Pre-defined theme colors

## Accessibility

- Uses Recharts' `accessibilityLayer` prop for keyboard navigation
- Tooltips provide screen reader-friendly descriptions of data points
- Color indicators supplement with text labels in tooltips/legends
- CSS variables respect color mode preferences

## Best Practices

- Set explicit height via className (e.g., "h-[400px]") - default is aspect-video
- Use `satisfies ChartConfig` for type-safe configuration
- Reference colors with `var(--color-{key})` to match config
- Include `accessibilityLayer` prop on Recharts components
- Format large numbers in tooltips with `formatter` prop
- Use theme-aware colors for charts that will be viewed in both modes

## Related

- [Recharts Documentation](https://recharts.org/) - Full chart component reference
- Card - Container for chart sections with headers
- Tooltip - Related tooltip pattern for non-chart UI

---

# Checkbox

Binary selection component for forms, settings, and multi-select interfaces.

## Import

```tsx
import { Checkbox } from "@neynar/ui/checkbox"
```

## Anatomy

```tsx
<Checkbox />
```

The component includes a built-in indicator with check icon - no need for separate child components.

## Props

All Base UI Checkbox.Root props are supported. Common props:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| checked | boolean | - | Controlled checked state |
| defaultChecked | boolean | false | Initial checked state (uncontrolled) |
| onCheckedChange | (checked: boolean) => void | - | Called when state changes |
| disabled | boolean | false | Prevents user interaction |
| required | boolean | false | Must be checked before form submission |
| name | string | - | Form field name |
| value | string | - | Form value when checked |
| indeterminate | boolean | false | Show indeterminate/mixed state |
| readOnly | boolean | false | Prevents state changes but shows value |

### Styling Props

| Prop | Type | Description |
|------|------|-------------|
| className | string | Additional CSS classes |

## Data Attributes

Use these for custom styling:

| Attribute | When Present |
|-----------|--------------|
| data-checked | Checkbox is checked |
| data-unchecked | Checkbox is unchecked |
| data-disabled | Component is disabled |
| data-readonly | Component is readonly |
| data-required | Field is required |
| data-invalid | Validation failed (with Field) |
| data-focused | Checkbox has focus |

## Examples

### Basic Usage

```tsx
<label className="flex items-center gap-2">
  <Checkbox />
  <span>Accept terms and conditions</span>
</label>
```

### Controlled State

```tsx
function NewsletterSubscription() {
  const [subscribed, setSubscribed] = useState(false)

  return (
    <label className="flex items-center gap-2">
      <Checkbox
        checked={subscribed}
        onCheckedChange={(checked) => setSubscribed(checked as boolean)}
      />
      <span>Subscribe to newsletter</span>
    </label>
  )
}
```

### With Label Component

```tsx
import { Label } from "@neynar/ui/label"

<label className="flex items-center gap-2">
  <Checkbox defaultChecked />
  <Label>Enable notifications</Label>
</label>
```

### With Description

```tsx
<label className="flex items-start gap-3">
  <Checkbox defaultChecked />
  <div className="flex-1">
    <Label>Email notifications</Label>
    <p className="text-muted-foreground text-xs">
      Receive email updates about your account activity
    </p>
  </div>
</label>
```

### Form Integration

```tsx
<form>
  <fieldset className="space-y-3">
    <legend className="text-sm font-medium">Preferences</legend>

    <label className="flex items-center gap-2">
      <Checkbox name="weekly-summary" defaultChecked />
      <Label>Send weekly summaries</Label>
    </label>

    <label className="flex items-center gap-2">
      <Checkbox name="dark-mode" />
      <Label>Enable dark mode</Label>
    </label>

    <label className="flex items-center gap-2">
      <Checkbox name="public-profile" required />
      <Label>I accept the terms of service *</Label>
    </label>
  </fieldset>
</form>
```

### Disabled State

```tsx
<label className="flex items-center gap-2">
  <Checkbox disabled defaultChecked />
  <Label>Admin access (requires Pro plan)</Label>
</label>
```

### Permission Settings Example

```tsx
function APIPermissions() {
  const [permissions, setPermissions] = useState({
    readAccess: true,
    writeAccess: false,
    adminAccess: false,
  })

  const togglePermission = (key: keyof typeof permissions) => {
    setPermissions(prev => ({ ...prev, [key]: !prev[key] }))
  }

  return (
    <div className="space-y-3">
      <label className="flex items-start gap-3">
        <Checkbox
          checked={permissions.readAccess}
          onCheckedChange={() => togglePermission('readAccess')}
        />
        <div className="flex-1">
          <Label>Read Access</Label>
          <p className="text-muted-foreground text-xs">
            Fetch casts, users, and channel data
          </p>
        </div>
      </label>

      <label className="flex items-start gap-3">
        <Checkbox
          checked={permissions.writeAccess}
          onCheckedChange={() => togglePermission('writeAccess')}
        />
        <div className="flex-1">
          <Label>Write Access</Label>
          <p className="text-muted-foreground text-xs">
            Publish casts and update profiles
          </p>
        </div>
      </label>

      <label className="flex items-start gap-3">
        <Checkbox
          checked={permissions.adminAccess}
          onCheckedChange={() => togglePermission('adminAccess')}
          disabled
        />
        <div className="flex-1">
          <Label>Admin Access</Label>
          <p className="text-muted-foreground text-xs">
            Full administrative permissions (Enterprise only)
          </p>
        </div>
      </label>
    </div>
  )
}
```

## Keyboard

| Key | Action |
|-----|--------|
| Space | Toggle checked state |
| Enter | Toggle checked state (in forms) |
| Tab | Move focus to next element |
| Shift + Tab | Move focus to previous element |

## Accessibility

- Renders semantic HTML with hidden native checkbox input
- Full keyboard navigation support
- Proper ARIA attributes automatically applied
- Works with form labels via native label element
- Supports required field validation
- Focus visible states for keyboard users

## Styling Notes

- Size: 16px × 16px (size-4)
- Border radius: 4px
- Checked state shows primary color background
- Includes focus ring on keyboard focus
- Disabled state reduces opacity to 50%
- Peer class allows styling adjacent elements based on state

## Related

- [Label](./label.llm.md) - Accessible labels for form fields
- [Switch](./switch.llm.md) - Alternative for on/off toggles
- [Radio Group](./radio-group.llm.md) - Single selection from multiple options

---

# Code

Inline code component with monospace font and subtle background.

## Import

```tsx
import { Code } from "@neynar/ui/typography"
```

## Usage

```tsx
<Text>
  Use the <Code>useState</Code> hook for local state.
</Text>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| color | "default" \| "muted" \| "subtle" \| "destructive" \| "success" \| "warning" \| "info" | "default" | Text color |
| className | string | - | Additional CSS classes |

## Examples

### Basic

```tsx
<Code>npm install @neynar/ui</Code>
```

### Inline with Text

```tsx
<Text>
  The <Code>Button</Code> component accepts a <Code>variant</Code> prop.
</Text>
```

### With Colors

```tsx
<Code color="destructive">error.message</Code>
<Code color="success">response.data</Code>
```

### In Lists

```tsx
<ul>
  <li><Code>variant</Code> - Visual style</li>
  <li><Code>size</Code> - Component size</li>
  <li><Code>disabled</Code> - Disable interaction</li>
</ul>
```

## Styling

Default styling:
- Monospace font (`font-mono`)
- Small text size (`text-sm`)
- Muted background (`bg-muted`)
- Rounded corners
- Horizontal padding

## Note

For multi-line code blocks with syntax highlighting, use a dedicated solution like `prism-react-renderer` or `shiki`.

## Related

- [Text](./text.llm.md) - Paragraph text
- [Kbd](./kbd.llm.md) - Keyboard shortcuts

---

# Collapsible

Expandable sections that show and hide content with smooth animations.

## Import

```tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@neynar/ui/collapsible"
```

## Anatomy

```tsx
<Collapsible>
  <CollapsibleTrigger />
  <CollapsibleContent />
</Collapsible>
```

## Components

| Component | Description |
|-----------|-------------|
| Collapsible | Root container, manages open/closed state |
| CollapsibleTrigger | Button that toggles the content visibility |
| CollapsibleContent | Panel that shows/hides with animation |

## Props

### Collapsible

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | - | Controlled open state |
| defaultOpen | boolean | - | Initial open state (uncontrolled) |
| onOpenChange | (open: boolean, details: object) => void | - | Called when open state changes |
| disabled | boolean | - | Disables all interactions |
| className | string \| function | - | CSS class or function returning class based on state |
| style | CSSProperties \| function | - | Style object or function returning styles based on state |
| render | ReactElement \| function | - | Custom render element or function |

### CollapsibleTrigger

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| nativeButton | boolean | true | Whether to render a native `<button>` element |
| className | string \| function | - | CSS class or function returning class based on state |
| style | CSSProperties \| function | - | Style object or function returning styles based on state |
| render | ReactElement \| function | - | Custom render element or function |

**Note**: Set `nativeButton={false}` if using `render` to replace with a non-button element (e.g., `<div>`).

### CollapsibleContent

Automatically animates height when opening/closing using CSS variables.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| keepMounted | boolean | false | Keep element in DOM when closed |
| hiddenUntilFound | boolean | false | Enable browser's page search to find and expand content |
| className | string \| function | - | CSS class or function returning class based on state |
| style | CSSProperties \| function | - | Style object or function returning styles based on state |
| render | ReactElement \| function | - | Custom render element or function |

**Note**: `hiddenUntilFound` overrides `keepMounted` and uses `hidden="until-found"` attribute.

### Render Prop

All components support the `render` prop to customize the rendered element:

```tsx
<CollapsibleTrigger render={<Button variant="outline" />}>
  Toggle Settings
</CollapsibleTrigger>
```

## Data Attributes

Use these for styling based on component state:

| Attribute | When Present | Where |
|-----------|--------------|-------|
| data-panel-open | Collapsible is open | Trigger |
| data-starting-style | Panel is animating in | Content |
| data-ending-style | Panel is animating out | Content |

## CSS Variables

Available for custom animations:

| Variable | Description | Where |
|----------|-------------|-------|
| --collapsible-panel-height | Current panel height | Content |
| --collapsible-panel-width | Current panel width | Content |

## Examples

### Basic Usage

```tsx
<Collapsible defaultOpen>
  <CollapsibleTrigger className="flex items-center justify-between border rounded-lg p-4">
    <span>Click to toggle</span>
    <ChevronDownIcon />
  </CollapsibleTrigger>
  <CollapsibleContent>
    <div className="border-x border-b p-4">
      Hidden content that slides open
    </div>
  </CollapsibleContent>
</Collapsible>
```

### Controlled State

```tsx
function Settings() {
  const [open, setOpen] = useState(false)

  return (
    <div>
      <Button onClick={() => setOpen(!open)}>Toggle</Button>

      <Collapsible open={open} onOpenChange={setOpen}>
        <CollapsibleTrigger>
          Settings {open ? <ChevronUpIcon /> : <ChevronDownIcon />}
        </CollapsibleTrigger>
        <CollapsibleContent>
          <div>Advanced settings panel</div>
        </CollapsibleContent>
      </Collapsible>
    </div>
  )
}
```

### Settings Panel with Icons

```tsx
<Collapsible>
  <CollapsibleTrigger className="flex items-center justify-between border rounded-lg p-4">
    <div className="flex items-center gap-3">
      <div className="bg-primary/10 text-primary rounded-md p-2">
        <SettingsIcon className="size-5" />
      </div>
      <div>
        <p className="font-medium">API Settings</p>
        <p className="text-sm text-muted-foreground">Configure rate limits</p>
      </div>
    </div>
    <ChevronDownIcon className="size-5" />
  </CollapsibleTrigger>
  <CollapsibleContent>
    <div className="border-x border-b p-4">
      <Label htmlFor="rate-limit">Rate Limit</Label>
      <Input id="rate-limit" type="number" defaultValue="10000" />
    </div>
  </CollapsibleContent>
</Collapsible>
```

### Multiple Independent Sections

```tsx
function AdvancedSettings() {
  return (
    <div className="space-y-3">
      <Collapsible>
        <CollapsibleTrigger className="w-full border rounded-lg p-4">
          API Settings
        </CollapsibleTrigger>
        <CollapsibleContent>
          <div className="border-x border-b p-4">API configuration</div>
        </CollapsibleContent>
      </Collapsible>

      <Collapsible>
        <CollapsibleTrigger className="w-full border rounded-lg p-4">
          Security
        </CollapsibleTrigger>
        <CollapsibleContent>
          <div className="border-x border-b p-4">Security settings</div>
        </CollapsibleContent>
      </Collapsible>
    </div>
  )
}
```

### Nested Collapsibles

```tsx
<Collapsible defaultOpen>
  <CollapsibleTrigger className="border rounded-lg p-4">
    Integrations
  </CollapsibleTrigger>
  <CollapsibleContent>
    <div className="border-x border-b p-4 space-y-2">
      <Collapsible>
        <CollapsibleTrigger className="border rounded p-3 text-sm">
          Email Integration
        </CollapsibleTrigger>
        <CollapsibleContent>
          <div className="mt-1 border rounded p-3">
            Configure email settings
          </div>
        </CollapsibleContent>
      </Collapsible>

      <Collapsible>
        <CollapsibleTrigger className="border rounded p-3 text-sm">
          Webhooks
        </CollapsibleTrigger>
        <CollapsibleContent>
          <div className="mt-1 border rounded p-3">
            Configure webhooks
          </div>
        </CollapsibleContent>
      </Collapsible>
    </div>
  </CollapsibleContent>
</Collapsible>
```

### Animated Chevron with Data Attributes

```tsx
<Collapsible>
  <CollapsibleTrigger className="group border rounded-lg p-4">
    <ChevronDownIcon className="transition-transform group-data-[panel-open]:rotate-180" />
    Expand Section
  </CollapsibleTrigger>
  <CollapsibleContent>
    <div className="border-x border-b p-4">Content</div>
  </CollapsibleContent>
</Collapsible>
```

### Disabled State

```tsx
<Collapsible disabled>
  <CollapsibleTrigger className="border rounded-lg p-4 opacity-50 cursor-not-allowed">
    Disabled Section
  </CollapsibleTrigger>
  <CollapsibleContent>
    <div className="border-x border-b p-4">Cannot be opened</div>
  </CollapsibleContent>
</Collapsible>
```

## Keyboard

| Key | Action |
|-----|--------|
| Space | Toggle when trigger is focused |
| Enter | Toggle when trigger is focused |

## Accessibility

- Trigger automatically receives `role="button"` and proper ARIA attributes
- Content visibility controlled with proper ARIA states
- Focus management handled automatically
- Keyboard navigation fully supported with Space/Enter keys
- Use `hiddenUntilFound` prop to enable browser's Ctrl+F/Cmd+F page search to find and expand collapsed content

## Related

- [Accordion](/components/accordion.llm.md) - For mutually exclusive collapsible sections
- [Dialog](/components/dialog.llm.md) - For modal content overlays
- [Tabs](/components/tabs.llm.md) - For switching between different views

---

# Combobox

Searchable select component supporting single and multi-select with optional chips UI.

## Import

```tsx
import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxList,
  ComboboxItem,
  ComboboxGroup,
  ComboboxLabel,
  ComboboxEmpty,
  ComboboxSeparator,
  ComboboxChips,
  ComboboxChip,
  ComboboxChipsInput,
  useComboboxAnchor,
} from "@neynar/ui/combobox"
```

## Anatomy

### Single Select
```tsx
<Combobox>
  <ComboboxInput />
  <ComboboxContent>
    <ComboboxList>
      <ComboboxEmpty />
      <ComboboxItem />
    </ComboboxList>
  </ComboboxContent>
</Combobox>
```

### Multi-Select with Chips
```tsx
const anchorRef = useComboboxAnchor()

<Combobox multiple>
  <ComboboxChips ref={anchorRef}>
    <ComboboxChip />
    <ComboboxChipsInput />
  </ComboboxChips>
  <ComboboxContent anchor={anchorRef}>
    <ComboboxList>
      <ComboboxItem />
    </ComboboxList>
  </ComboboxContent>
</Combobox>
```

## Components

| Component | Description |
|-----------|-------------|
| Combobox | Root component managing state (doesn't render element) |
| ComboboxInput | Text input with optional trigger/clear buttons for single-select |
| ComboboxContent | Popup container with automatic portal and positioning |
| ComboboxList | Scrollable container for items with overflow handling |
| ComboboxItem | Selectable item with check indicator when selected |
| ComboboxChips | Container for selected chips in multi-select mode |
| ComboboxChip | Individual chip with optional remove button |
| ComboboxChipsInput | Text input for multi-select (use inside ComboboxChips) |
| ComboboxGroup | Groups related items together |
| ComboboxLabel | Label for item groups |
| ComboboxEmpty | Message shown when no results match search |
| ComboboxSeparator | Visual divider between groups |
| useComboboxAnchor | Hook returning ref for anchoring popup to chips |

## Props

### Combobox

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string \| string[] \| null | - | Controlled selected value(s) |
| onValueChange | (value) => void | - | Called when selection changes |
| defaultValue | string \| string[] \| null | - | Uncontrolled initial value |
| inputValue | string | - | Controlled input text value |
| onInputValueChange | (value) => void | - | Called when input text changes |
| defaultInputValue | string | - | Uncontrolled initial input text |
| multiple | boolean | false | Enable multi-select mode |
| disabled | boolean | false | Disable all interactions |
| open | boolean | - | Controlled open state |
| onOpenChange | (open) => void | - | Called when popup opens/closes |
| openOnInputClick | boolean | true | Whether clicking input opens popup |

### ComboboxInput

Single-select input wrapped in InputGroup with optional addons.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| showTrigger | boolean | true | Show dropdown trigger button |
| showClear | boolean | false | Show clear button when value selected |
| placeholder | string | - | Input placeholder text |
| disabled | boolean | false | Disable input |

### ComboboxContent

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| side | 'top' \| 'bottom' \| 'left' \| 'right' | 'bottom' | Popup placement side |
| align | 'start' \| 'center' \| 'end' | 'start' | Popup alignment |
| sideOffset | number | 6 | Distance from anchor |
| alignOffset | number | 0 | Alignment adjustment |
| anchor | RefObject | - | Ref to anchor element (for chips mode) |

**Anchoring for Multi-Select:**
```tsx
const anchorRef = useComboboxAnchor()
<ComboboxChips ref={anchorRef}>...</ComboboxChips>
<ComboboxContent anchor={anchorRef}>...</ComboboxContent>
```

### ComboboxItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | any | - | **Required**. Unique value identifying this item |
| disabled | boolean | false | Disable item interaction |
| onClick | MouseEventHandler | - | Click handler for item selection |

### ComboboxChips

Container for chips in multi-select mode. Must be used with `useComboboxAnchor()` hook.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| ref | RefObject | - | Pass ref from useComboboxAnchor() |
| children | ReactNode | - | ComboboxChip and ComboboxChipsInput elements |

### ComboboxChip

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| showRemove | boolean | true | Show X button to remove chip |
| children | ReactNode | - | Chip content (usually text/icon) |

### ComboboxChipsInput

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| placeholder | string | - | Input placeholder text |

## Data Attributes

### ComboboxInput
- `data-popup-open` - Popup is open
- `data-disabled` - Input is disabled
- `data-focused` - Input has focus

### ComboboxContent
- `data-open` - Popup is open (triggers animations)
- `data-closed` - Popup is closed (triggers animations)
- `data-side` - Current placement side (top/bottom/left/right)
- `data-chips` - Anchored to chips container (multi-select)

### ComboboxItem
- `data-selected` - Item is selected
- `data-highlighted` - Item is keyboard highlighted
- `data-disabled` - Item is disabled

### ComboboxList
- `data-empty` - List has no items (shows ComboboxEmpty)

## Examples

### Basic Single Select

```tsx
function FrameworkPicker() {
  const [value, setValue] = useState<string | null>(null)

  const frameworks = [
    { value: "next", label: "Next.js" },
    { value: "react", label: "React" },
    { value: "vue", label: "Vue" },
  ]

  return (
    <Combobox value={value} onValueChange={(v) => setValue(v as string)}>
      <ComboboxInput placeholder="Select framework..." showClear />
      <ComboboxContent>
        <ComboboxList>
          <ComboboxEmpty>No frameworks found</ComboboxEmpty>
          {frameworks.map((fw) => (
            <ComboboxItem key={fw.value} value={fw.value}>
              {fw.label}
            </ComboboxItem>
          ))}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

### Multi-Select with Chips

```tsx
function TagPicker() {
  const [tags, setTags] = useState<string[]>([])
  const anchorRef = useComboboxAnchor()

  const availableTags = ["react", "typescript", "nextjs", "tailwind"]

  return (
    <Combobox multiple value={tags} onValueChange={(v) => setTags(v as string[])}>
      <ComboboxChips ref={anchorRef}>
        {tags.map((tag) => (
          <ComboboxChip key={tag}>{tag}</ComboboxChip>
        ))}
        <ComboboxChipsInput placeholder="Add tags..." />
      </ComboboxChips>
      <ComboboxContent anchor={anchorRef}>
        <ComboboxList>
          <ComboboxEmpty>No tags found</ComboboxEmpty>
          {availableTags.map((tag) => (
            <ComboboxItem key={tag} value={tag}>
              {tag}
            </ComboboxItem>
          ))}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

### Grouped Items with Labels

```tsx
function FoodPicker() {
  const [value, setValue] = useState<string | null>(null)

  return (
    <Combobox value={value} onValueChange={(v) => setValue(v as string)}>
      <ComboboxInput placeholder="Select food..." showClear />
      <ComboboxContent>
        <ComboboxList>
          <ComboboxEmpty>No food found</ComboboxEmpty>
          <ComboboxGroup>
            <ComboboxLabel>Fruits</ComboboxLabel>
            <ComboboxItem value="apple">Apple</ComboboxItem>
            <ComboboxItem value="banana">Banana</ComboboxItem>
          </ComboboxGroup>
          <ComboboxSeparator />
          <ComboboxGroup>
            <ComboboxLabel>Vegetables</ComboboxLabel>
            <ComboboxItem value="carrot">Carrot</ComboboxItem>
            <ComboboxItem value="broccoli">Broccoli</ComboboxItem>
          </ComboboxGroup>
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

### Items with Icons and Metadata

```tsx
function ChannelPicker() {
  const [channel, setChannel] = useState<string | null>(null)

  const channels = [
    { id: "1", name: "farcaster", subscribers: 89450 },
    { id: "2", name: "base", subscribers: 45230 },
  ]

  return (
    <Combobox value={channel} onValueChange={(v) => setChannel(v as string)}>
      <ComboboxInput placeholder="Search channels..." showClear />
      <ComboboxContent>
        <ComboboxList>
          <ComboboxEmpty>No channels found</ComboboxEmpty>
          {channels.map((ch) => (
            <ComboboxItem key={ch.id} value={ch.id}>
              <HashIcon className="text-muted-foreground size-4" />
              <div className="flex-1">
                <div className="font-medium">/{ch.name}</div>
                <div className="text-muted-foreground text-xs">
                  {ch.subscribers.toLocaleString()} subscribers
                </div>
              </div>
            </ComboboxItem>
          ))}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

### Without Trigger Button

```tsx
<Combobox>
  <ComboboxInput
    placeholder="Type to search..."
    showTrigger={false}
    showClear
  />
  <ComboboxContent>
    <ComboboxList>
      <ComboboxItem value="item1">Item 1</ComboboxItem>
    </ComboboxList>
  </ComboboxContent>
</Combobox>
```

## Keyboard Interactions

| Key | Action |
|-----|--------|
| ArrowDown | Highlight next item (opens popup if closed) |
| ArrowUp | Highlight previous item (opens popup if closed) |
| Enter | Select highlighted item and close popup |
| Escape | Close popup and clear highlight |
| Home | Highlight first item |
| End | Highlight last item |
| Tab | Close popup and move focus |
| Backspace | Remove last chip (multi-select, when input empty) |

## Accessibility

- ARIA combobox pattern with `role="combobox"` on input
- `aria-expanded` indicates popup state
- `aria-controls` links input to popup listbox
- `aria-activedescendant` tracks highlighted item
- Items use `role="option"` with `aria-selected` state
- Automatic focus management when opening/closing
- Screen readers announce selection changes and item counts
- Keyboard navigation follows ARIA authoring practices

## Related

- [Select](/components/select.llm.md) - Simple dropdown without search
- [Input](/components/input.llm.md) - Basic text input
- [Command](/components/command.llm.md) - Command palette pattern

---

# Command

Fast, accessible command palette with keyboard navigation and fuzzy search built on cmdk.

## Import

```tsx
import {
  Command,
  CommandDialog,
  CommandInput,
  CommandList,
  CommandEmpty,
  CommandGroup,
  CommandItem,
  CommandSeparator,
  CommandShortcut,
} from "@neynar/ui/command"
```

## Anatomy

```tsx
<CommandDialog open={open} onOpenChange={setOpen}>
  <Command>
    <CommandInput placeholder="Search..." />
    <CommandList>
      <CommandEmpty>No results found.</CommandEmpty>
      <CommandGroup heading="Navigation">
        <CommandItem onSelect={handleSelect}>
          Dashboard
          <CommandShortcut>⌘D</CommandShortcut>
        </CommandItem>
      </CommandGroup>
      <CommandSeparator />
      <CommandGroup heading="Actions">
        <CommandItem>Create</CommandItem>
      </CommandGroup>
    </CommandList>
  </Command>
</CommandDialog>
```

## Components

| Component | Description |
|-----------|-------------|
| Command | Root container with fuzzy search and keyboard navigation |
| CommandDialog | Command wrapped in modal dialog, typical for ⌘K palettes |
| CommandInput | Search input with integrated search icon |
| CommandList | Scrollable container for groups/items (max-h-72) |
| CommandEmpty | Displayed when search returns no results |
| CommandGroup | Groups related items with optional heading |
| CommandItem | Selectable item, automatically shows check icon when selected |
| CommandSeparator | Visual divider between groups |
| CommandShortcut | Keyboard shortcut label, auto-aligned right |

## Props

### Command

Extends cmdk's Command component.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| label | string | - | Accessible label for screen readers |
| value | string | - | Controlled selected value |
| onValueChange | (value: string) => void | - | Called when selection changes |
| filter | (value: string, search: string) => number | - | Custom filter function (0-1 score) |
| className | string | - | Additional CSS classes |

### CommandDialog

Wraps Command in a modal dialog with portal and overlay.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | - | Controlled open state |
| onOpenChange | (open: boolean) => void | - | Called when open state changes |
| title | string | "Command Palette" | Dialog title (screen readers only) |
| description | string | "Search for a command..." | Dialog description (screen readers only) |
| showCloseButton | boolean | false | Show X button in top-right corner |
| className | string | - | Additional CSS classes for content |

Title and description are visually hidden but read by screen readers for accessibility.

### CommandInput

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| placeholder | string | - | Input placeholder text |
| value | string | - | Controlled input value |
| onValueChange | (search: string) => void | - | Called when input changes |
| className | string | - | Additional CSS classes |

Search icon is automatically included on the right side of the input.

### CommandList

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |

Automatically handles scrolling with max height of 18rem (72). Hidden scrollbar for clean appearance.

### CommandEmpty

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | - | Content shown when no results |
| className | string | - | Additional CSS classes |

Automatically displayed when search returns no matching items.

### CommandGroup

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| heading | string | - | Optional group label |
| className | string | - | Additional CSS classes |

### CommandItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | - | Unique value (auto-inferred from textContent if omitted) |
| keywords | string[] | - | Additional search keywords |
| onSelect | (value: string) => void | - | Called when item is selected |
| disabled | boolean | - | Disable selection |
| className | string | - | Additional CSS classes |

Check icon automatically appears when item is selected (hidden if CommandShortcut is present).

### CommandShortcut

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | - | Shortcut text (e.g., "⌘K") |
| className | string | - | Additional CSS classes |

Automatically positioned at the end of the command item and hides the check icon.

### CommandSeparator

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |

## Data Attributes

### CommandItem

| Attribute | When Present |
|-----------|--------------|
| data-selected | Item is currently highlighted/active |
| data-disabled | Item is disabled (disabled={true}) |
| data-checked | Item is marked as checked/selected |

Use these for custom styling based on item state.

## Examples

### Basic Command Palette

```tsx
function QuickActions() {
  const [open, setOpen] = useState(false)

  useEffect(() => {
    const down = (e: KeyboardEvent) => {
      if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault()
        setOpen((open) => !open)
      }
    }
    document.addEventListener("keydown", down)
    return () => document.removeEventListener("keydown", down)
  }, [])

  return (
    <CommandDialog open={open} onOpenChange={setOpen}>
      <Command>
        <CommandInput placeholder="Type a command or search..." />
        <CommandList>
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandGroup heading="Actions">
            <CommandItem onSelect={() => console.log("Dashboard")}>
              <LayoutDashboardIcon />
              Dashboard
              <CommandShortcut>⌘D</CommandShortcut>
            </CommandItem>
            <CommandItem onSelect={() => console.log("Settings")}>
              <SettingsIcon />
              Settings
              <CommandShortcut>⌘,</CommandShortcut>
            </CommandItem>
          </CommandGroup>
        </CommandList>
      </Command>
    </CommandDialog>
  )
}
```

### Multiple Groups with Separators

```tsx
<Command>
  <CommandInput placeholder="Search..." />
  <CommandList>
    <CommandEmpty>No results found.</CommandEmpty>

    <CommandGroup heading="Navigation">
      <CommandItem>
        <LayoutDashboardIcon />
        Dashboard
      </CommandItem>
      <CommandItem>
        <BarChart3Icon />
        Analytics
      </CommandItem>
    </CommandGroup>

    <CommandSeparator />

    <CommandGroup heading="Settings">
      <CommandItem>
        <UserIcon />
        Profile
      </CommandItem>
      <CommandItem>
        <SettingsIcon />
        Preferences
      </CommandItem>
    </CommandGroup>
  </CommandList>
</Command>
```

### Controlled with Loading States

```tsx
function CommandWithLoading() {
  const [open, setOpen] = useState(false)
  const [loading, setLoading] = useState(false)
  const [selectedCommand, setSelectedCommand] = useState<string | null>(null)

  const handleSelect = (value: string) => {
    setSelectedCommand(value)
    setLoading(true)

    // Simulate async action
    setTimeout(() => {
      setLoading(false)
      setOpen(false)
      setSelectedCommand(null)
    }, 1000)
  }

  return (
    <CommandDialog open={open} onOpenChange={setOpen}>
      <Command>
        <CommandInput placeholder="Type a command..." />
        <CommandList>
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandGroup heading="Actions">
            <CommandItem
              onSelect={() => handleSelect("create")}
              disabled={loading && selectedCommand === "create"}
            >
              {loading && selectedCommand === "create" ? (
                <Loader2Icon className="animate-spin" />
              ) : (
                <ZapIcon />
              )}
              Create API Key
              <CommandShortcut>⌘N</CommandShortcut>
            </CommandItem>
          </CommandGroup>
        </CommandList>
      </Command>
    </CommandDialog>
  )
}
```

### Custom Empty State

```tsx
<Command>
  <CommandInput placeholder="Search documentation..." />
  <CommandList>
    <CommandEmpty>
      <div className="py-6 text-center">
        <p className="text-sm">No results found.</p>
        <p className="text-muted-foreground mt-1 text-xs">
          Try a different search term or check the docs.
        </p>
      </div>
    </CommandEmpty>
    <CommandGroup heading="Documentation">
      <CommandItem>Getting Started</CommandItem>
      <CommandItem>API Reference</CommandItem>
    </CommandGroup>
  </CommandList>
</Command>
```

### Without Dialog (Inline)

```tsx
<Command className="w-full max-w-md rounded-lg border">
  <CommandInput placeholder="Search commands..." />
  <CommandList>
    <CommandEmpty>No results found.</CommandEmpty>
    <CommandGroup heading="Actions">
      <CommandItem>
        <FileTextIcon />
        New File
      </CommandItem>
      <CommandItem>
        <UserIcon />
        New User
      </CommandItem>
    </CommandGroup>
  </CommandList>
</Command>
```

## Keyboard

| Key | Action |
|-----|--------|
| ⌘K / Ctrl+K | Open command palette (app implementation) |
| ↓ / ↑ | Navigate items |
| Enter | Select highlighted item |
| Escape | Close dialog |
| Home / End | Jump to first/last item |
| Type | Filter items by fuzzy search |

## Accessibility

- Full keyboard navigation with arrow keys and Enter
- ARIA attributes automatically applied to items and groups
- Screen reader support for search input and results count
- Focus management when opening/closing dialog
- DialogTitle and DialogDescription provided for screen readers (visually hidden)
- Disabled items marked with `aria-disabled` and cannot be selected

## Related

- [Dialog](/Users/bobringer/work/neynar/worktrees/frontend-monorepo/neyn-8162-nas-baseui-upgrade./dialog.llm.md) - Command dialog uses Dialog internally
- [Input Group](/Users/bobringer/work/neynar/worktrees/frontend-monorepo/neyn-8162-nas-baseui-upgrade./input-group.llm.md) - CommandInput uses InputGroup for icon layout

---

# ContextMenu

Right-click context menu with support for nested submenus, checkboxes, radio groups, keyboard shortcuts, and destructive actions.

## Import

```tsx
import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuCheckboxItem,
  ContextMenuRadioItem,
  ContextMenuRadioGroup,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuGroup,
  ContextMenuSub,
  ContextMenuSubTrigger,
  ContextMenuSubContent,
} from "@neynar/ui/context-menu"
```

## Anatomy

```tsx
<ContextMenu>
  <ContextMenuTrigger>Right-click target</ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuGroup>
      <ContextMenuLabel>Section</ContextMenuLabel>
      <ContextMenuItem>Action</ContextMenuItem>
      <ContextMenuCheckboxItem>Toggle</ContextMenuCheckboxItem>
    </ContextMenuGroup>
    <ContextMenuSeparator />
    <ContextMenuRadioGroup>
      <ContextMenuRadioItem value="option">Option</ContextMenuRadioItem>
    </ContextMenuRadioGroup>
    <ContextMenuSub>
      <ContextMenuSubTrigger>More</ContextMenuSubTrigger>
      <ContextMenuSubContent>
        <ContextMenuItem>Nested action</ContextMenuItem>
      </ContextMenuSubContent>
    </ContextMenuSub>
  </ContextMenuContent>
</ContextMenu>
```

## Components

| Component | Description |
|-----------|-------------|
| ContextMenu | Root container, manages open/closed state |
| ContextMenuTrigger | Element that opens menu on right-click |
| ContextMenuContent | Menu popup with automatic portal and positioning |
| ContextMenuItem | Interactive menu action |
| ContextMenuCheckboxItem | Menu item with checkbox state |
| ContextMenuRadioItem | Menu item for mutually exclusive selection |
| ContextMenuRadioGroup | Container for radio items |
| ContextMenuLabel | Non-interactive section label |
| ContextMenuSeparator | Visual divider between items |
| ContextMenuShortcut | Keyboard shortcut hint (right-aligned) |
| ContextMenuGroup | Groups related items |
| ContextMenuSub | Root for nested submenu |
| ContextMenuSubTrigger | Item that opens submenu |
| ContextMenuSubContent | Submenu popup content |

## Props

### ContextMenu

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | - | Controlled open state |
| onOpenChange | (open: boolean) => void | - | Called when open state changes |
| defaultOpen | boolean | false | Uncontrolled initial open state |

### ContextMenuContent

Automatically renders portal and positioner.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| align | "start" \| "end" \| "center" | "start" | Alignment relative to trigger |
| alignOffset | number | 4 | Offset from alignment edge (px) |
| side | "top" \| "right" \| "bottom" \| "left" | "right" | Which side to position menu |
| sideOffset | number | 0 | Offset from trigger (px) |

### ContextMenuItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "default" \| "destructive" \| "success" \| "warning" \| "info" | "default" | Visual style variant |
| inset | boolean | false | Extra left padding for alignment |
| disabled | boolean | false | Disables interaction |
| closeOnClick | boolean | true | Close menu when clicked |

### ContextMenuCheckboxItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| checked | boolean \| "indeterminate" | - | Controlled checked state |
| onCheckedChange | (checked: boolean) => void | - | Called when checked changes |
| disabled | boolean | false | Disables interaction |

### ContextMenuRadioGroup

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | - | Controlled selected value |
| onValueChange | (value: string) => void | - | Called when selection changes |

### ContextMenuRadioItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | - | Value for this radio option |
| disabled | boolean | false | Disables interaction |

### ContextMenuLabel

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| inset | boolean | false | Extra left padding for alignment |

### ContextMenuSubTrigger

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| inset | boolean | false | Extra left padding for alignment |

Automatically includes chevron icon.

## Data Attributes

| Attribute | When Present | Applied To |
|-----------|--------------|------------|
| data-open | Menu is open | ContextMenuContent |
| data-closed | Menu is closed | ContextMenuContent |
| data-highlighted | Item is keyboard-focused | ContextMenuItem, ContextMenuSubTrigger |
| data-disabled | Item is disabled | ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioItem |
| data-inset | Inset prop is true | ContextMenuLabel, ContextMenuItem, ContextMenuSubTrigger |
| data-side | Position side | ContextMenuContent |

## Variants

ContextMenuItem supports semantic variants:

| Variant | Use Case |
|---------|----------|
| default | Standard actions |
| destructive | Delete, remove, irreversible actions |
| success | Approve, confirm actions |
| warning | Actions requiring caution |
| info | Informational actions |

## Examples

### Basic Context Menu

```tsx
<ContextMenu>
  <ContextMenuTrigger>
    <div className="border p-4 rounded">Right-click here</div>
  </ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuItem>
      <EditIcon />
      Edit
    </ContextMenuItem>
    <ContextMenuItem>
      <CopyIcon />
      Copy
    </ContextMenuItem>
    <ContextMenuSeparator />
    <ContextMenuItem variant="destructive">
      <TrashIcon />
      Delete
    </ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>
```

### With Keyboard Shortcuts

```tsx
<ContextMenuContent>
  <ContextMenuItem>
    <EditIcon />
    Edit
    <ContextMenuShortcut>⌘E</ContextMenuShortcut>
  </ContextMenuItem>
  <ContextMenuItem>
    <CopyIcon />
    Copy
    <ContextMenuShortcut>⌘C</ContextMenuShortcut>
  </ContextMenuItem>
</ContextMenuContent>
```

### Grouped Items with Labels

```tsx
<ContextMenuContent>
  <ContextMenuGroup>
    <ContextMenuLabel>Actions</ContextMenuLabel>
    <ContextMenuItem>Edit</ContextMenuItem>
    <ContextMenuItem>Copy</ContextMenuItem>
  </ContextMenuGroup>
  <ContextMenuSeparator />
  <ContextMenuGroup>
    <ContextMenuLabel>Organization</ContextMenuLabel>
    <ContextMenuItem>Add to Favorites</ContextMenuItem>
    <ContextMenuItem>Pin</ContextMenuItem>
  </ContextMenuGroup>
</ContextMenuContent>
```

### Checkbox Items

```tsx
function ViewOptions() {
  const [showGrid, setShowGrid] = useState(true)
  const [showLabels, setShowLabels] = useState(false)

  return (
    <ContextMenuContent>
      <ContextMenuGroup>
        <ContextMenuLabel>View Options</ContextMenuLabel>
      </ContextMenuGroup>
      <ContextMenuCheckboxItem
        checked={showGrid}
        onCheckedChange={setShowGrid}
      >
        Show Grid
      </ContextMenuCheckboxItem>
      <ContextMenuCheckboxItem
        checked={showLabels}
        onCheckedChange={setShowLabels}
      >
        Show Labels
      </ContextMenuCheckboxItem>
    </ContextMenuContent>
  )
}
```

### Radio Group

```tsx
function SortMenu() {
  const [sortBy, setSortBy] = useState("name")

  return (
    <ContextMenuContent>
      <ContextMenuGroup>
        <ContextMenuLabel>Sort By</ContextMenuLabel>
      </ContextMenuGroup>
      <ContextMenuRadioGroup value={sortBy} onValueChange={setSortBy}>
        <ContextMenuRadioItem value="name">Name</ContextMenuRadioItem>
        <ContextMenuRadioItem value="date">Date</ContextMenuRadioItem>
        <ContextMenuRadioItem value="size">Size</ContextMenuRadioItem>
      </ContextMenuRadioGroup>
    </ContextMenuContent>
  )
}
```

### Nested Submenus

```tsx
<ContextMenuContent>
  <ContextMenuItem>Edit</ContextMenuItem>
  <ContextMenuItem>Copy</ContextMenuItem>
  <ContextMenuSeparator />
  <ContextMenuSub>
    <ContextMenuSubTrigger>
      <ShareIcon />
      Share
    </ContextMenuSubTrigger>
    <ContextMenuSubContent>
      <ContextMenuItem>
        <MailIcon />
        Email
      </ContextMenuItem>
      <ContextMenuItem>
        <CopyIcon />
        Copy Link
      </ContextMenuItem>
      <ContextMenuItem>
        <DownloadIcon />
        Download
      </ContextMenuItem>
    </ContextMenuSubContent>
  </ContextMenuSub>
</ContextMenuContent>
```

### Inset Alignment

Use `inset` prop to align items that don't have icons with items that do:

```tsx
<ContextMenuContent>
  <ContextMenuGroup>
    <ContextMenuLabel>Navigation</ContextMenuLabel>
    <ContextMenuItem>
      <UserIcon />
      Profile
    </ContextMenuItem>
    <ContextMenuItem inset>Account Settings</ContextMenuItem>
    <ContextMenuItem inset>Privacy Settings</ContextMenuItem>
  </ContextMenuGroup>
</ContextMenuContent>
```

### Variant Examples

```tsx
<ContextMenuContent>
  <ContextMenuItem>Default Action</ContextMenuItem>
  <ContextMenuSeparator />
  <ContextMenuItem variant="success">
    <CheckCircle2Icon />
    Approve
  </ContextMenuItem>
  <ContextMenuItem variant="warning">
    <AlertTriangleIcon />
    Mark for Review
  </ContextMenuItem>
  <ContextMenuItem variant="info">
    <InfoIcon />
    View Details
  </ContextMenuItem>
  <ContextMenuSeparator />
  <ContextMenuItem variant="destructive">
    <TrashIcon />
    Delete
  </ContextMenuItem>
</ContextMenuContent>
```

## Keyboard

| Key | Action |
|-----|--------|
| Space / Enter | Activate focused item |
| ArrowDown | Move focus to next item |
| ArrowUp | Move focus to previous item |
| ArrowRight | Open submenu (when focused on SubTrigger) |
| ArrowLeft | Close submenu |
| Escape | Close menu |
| Tab | Move focus out and close |

## Accessibility

- Right-click or Shift+F10 opens context menu
- ARIA roles: `menu`, `menuitem`, `menuitemcheckbox`, `menuitemradio`
- Focus is trapped within open menu
- Screen readers announce menu state, selected items, and keyboard shortcuts
- CheckboxItem and RadioItem include proper ARIA checked states

## Related

- [DropdownMenu](./dropdown-menu.llm.md) - Click-triggered menu
- [Menubar](./menubar.llm.md) - Application menu bar

---

# Dialog

Modal overlay component for capturing user attention, gathering input, or confirming actions.

## Import

```tsx
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@neynar/ui/dialog"
```

## Anatomy

```tsx
<Dialog>
  <DialogTrigger>Open</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Title</DialogTitle>
      <DialogDescription>Description</DialogDescription>
    </DialogHeader>
    {/* Content */}
    <DialogFooter>
      <DialogClose>Cancel</DialogClose>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

## Components

| Component | Description |
|-----------|-------------|
| Dialog | Root container that manages open state (controlled or uncontrolled) |
| DialogTrigger | Button that opens the dialog, supports `render` prop for customization |
| DialogContent | Main content container with automatic portal, overlay, and animations |
| DialogHeader | Container for title and description with consistent spacing |
| DialogTitle | Accessible heading element, required for screen readers |
| DialogDescription | Optional description text with support for inline links |
| DialogFooter | Action button container, stacks vertically on mobile |
| DialogClose | Button that closes the dialog, works with `render` prop |
| DialogOverlay | Backdrop overlay with blur effect (auto-included in DialogContent) |
| DialogPortal | Portal to document root (auto-included in DialogContent) |

## Props

### Dialog

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | - | Controlled open state |
| onOpenChange | (open: boolean) => void | - | Callback when open state changes |
| defaultOpen | boolean | - | Initial open state (uncontrolled) |

### DialogContent

Automatically renders into a portal with overlay backdrop. Centered on screen with fade and zoom animations.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| showCloseButton | boolean | true | Show close button (X) in top-right corner |
| className | string | - | Additional CSS classes |

### DialogFooter

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| showCloseButton | boolean | false | Render a "Close" button automatically |
| className | string | - | Additional CSS classes |

### Render Prop Pattern

DialogTrigger and DialogClose support the `render` prop to customize the underlying element:

```tsx
<DialogTrigger render={<Button variant="destructive" />}>
  Delete Account
</DialogTrigger>

<DialogClose render={<Button variant="outline" />}>
  Cancel
</DialogClose>
```

This allows you to use any component while preserving dialog functionality.

## Data Attributes

Applied automatically for styling and animations:

| Attribute | When Present | Usage |
|-----------|--------------|-------|
| data-open | Dialog is open | Styling open state, animations |
| data-closed | Dialog is closed | Styling closed state, exit animations |
| data-slot | Always | Component identification ("dialog", "dialog-trigger", etc.) |

## Examples

### Basic Dialog

```tsx
<Dialog>
  <DialogTrigger render={<Button />}>Open Dialog</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Dialog Title</DialogTitle>
      <DialogDescription>
        This is a description providing context about the dialog.
      </DialogDescription>
    </DialogHeader>
    <DialogFooter>
      <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
      <Button>Confirm</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

### Controlled State

```tsx
function ControlledDialog() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button onClick={() => setOpen(true)}>Open from Outside</Button>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogTrigger render={<Button />}>Open Dialog</DialogTrigger>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Controlled Dialog</DialogTitle>
            <DialogDescription>
              State is controlled by React state.
            </DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <Button onClick={() => setOpen(false)}>Close Programmatically</Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  )
}
```

### Form Dialog

```tsx
<Dialog>
  <DialogTrigger render={<Button />}>
    <PlusIcon data-icon="inline-start" />
    Create Project
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Create New Project</DialogTitle>
      <DialogDescription>
        Set up a new project to organize your API keys.
      </DialogDescription>
    </DialogHeader>
    <div className="space-y-4">
      <div className="space-y-2">
        <Label htmlFor="name">Project Name</Label>
        <Input id="name" placeholder="My Awesome Project" />
      </div>
      <div className="space-y-2">
        <Label htmlFor="description">Description</Label>
        <Input id="description" placeholder="Brief description" />
      </div>
    </div>
    <DialogFooter>
      <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
      <Button>
        <PlusIcon data-icon="inline-start" />
        Create Project
      </Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

### Destructive Confirmation

```tsx
function DeleteConfirmation() {
  const [isDeleting, setIsDeleting] = useState(false)

  function handleDelete() {
    setIsDeleting(true)
    // Perform delete operation
    setTimeout(() => setIsDeleting(false), 1500)
  }

  return (
    <Dialog>
      <DialogTrigger render={<Button variant="destructive" />}>
        <TrashIcon data-icon="inline-start" />
        Revoke Key
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Revoke API Key?</DialogTitle>
          <DialogDescription>
            This action cannot be undone. Revoking this key will immediately
            stop all applications using it from accessing the API.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
          <Button variant="destructive" onClick={handleDelete} disabled={isDeleting}>
            {isDeleting ? (
              <>
                <Loader2Icon data-icon="inline-start" className="animate-spin" />
                Revoking...
              </>
            ) : (
              <>
                <TrashIcon data-icon="inline-start" />
                Revoke Key
              </>
            )}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
```

### Without Close Button

For dialogs that require explicit user action:

```tsx
<Dialog>
  <DialogTrigger render={<Button />}>Forced Action</DialogTrigger>
  <DialogContent showCloseButton={false}>
    <DialogHeader>
      <DialogTitle>Action Required</DialogTitle>
      <DialogDescription>
        Please choose one of the options below. This dialog cannot be
        dismissed without making a selection.
      </DialogDescription>
    </DialogHeader>
    <DialogFooter>
      <DialogClose render={<Button variant="outline" />}>Option A</DialogClose>
      <DialogClose render={<Button />}>Option B</DialogClose>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

## Keyboard

| Key | Action |
|-----|--------|
| Escape | Closes the dialog (unless showCloseButton={false}) |
| Tab | Cycles through focusable elements within dialog |

## Accessibility

- Automatically sets `role="dialog"` and `aria-modal="true"`
- DialogTitle automatically provides `aria-labelledby` for the dialog
- DialogDescription provides `aria-describedby` when present
- Focus is trapped within the dialog when open
- Focus returns to trigger element when closed
- Escape key closes dialog by default

## Related

- **AlertDialog** - For simpler confirmations with pre-styled variants
- **Drawer** - For mobile-first slide-in panels
- **Popover** - For non-modal contextual overlays
- **Sheet** - For side-panel content

---

# Drawer

A sliding panel that appears from any screen edge (top, bottom, left, right) with drag-to-dismiss support, frosted glass overlay, and flexible composition.

## Import

```tsx
import {
  Drawer,
  DrawerTrigger,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerDescription,
  DrawerFooter,
  DrawerClose,
  DrawerPortal,
  DrawerOverlay,
} from "@neynar/ui/drawer"
```

## Anatomy

```tsx
<Drawer direction="right">
  <DrawerTrigger>Open</DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Title</DrawerTitle>
      <DrawerDescription>Description</DrawerDescription>
    </DrawerHeader>
    {/* Content */}
    <DrawerFooter>
      <DrawerClose>Cancel</DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>
```

## Components

| Component | Description |
|-----------|-------------|
| Drawer | Root container, manages open/close state |
| DrawerTrigger | Button or element that opens the drawer |
| DrawerContent | Panel with auto-portal, overlay, and drag handle (bottom drawers) |
| DrawerHeader | Header section, centered on top/bottom drawers |
| DrawerTitle | Accessible title announced when opened |
| DrawerDescription | Accessible description for screen readers |
| DrawerFooter | Footer with actions, auto-positioned at bottom |
| DrawerClose | Button or element that closes the drawer |
| DrawerPortal | Manual portal container (auto-used by DrawerContent) |
| DrawerOverlay | Frosted backdrop (auto-rendered by DrawerContent) |

## Props

### Drawer

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| direction | "top" \| "bottom" \| "left" \| "right" | "bottom" | Edge from which drawer slides |
| open | boolean | - | Controlled open state |
| defaultOpen | boolean | - | Initial open state (uncontrolled) |
| onOpenChange | (open: boolean) => void | - | Called when state changes |
| modal | boolean | true | Whether to block interaction with content behind |
| dismissible | boolean | true | Whether drawer can be closed by clicking outside or dragging |
| shouldScaleBackground | boolean | false | Whether to scale background when open |
| handleOnly | boolean | false | Only allow dragging from handle (not entire drawer) |

### DrawerContent

Automatically renders portal, overlay, and drag handle (bottom drawers only). Content panel is positioned based on `direction` prop:
- **bottom**: Slides up from bottom, 80vh max height, rounded top corners
- **top**: Slides down from top, 80vh max height, rounded bottom corners
- **left**: Slides from left, 75% width (max 24rem), rounded right corners
- **right**: Slides from right, 75% width (max 24rem), rounded left corners

Extends all standard div props via `React.ComponentProps<typeof DrawerPrimitive.Content>`.

### DrawerHeader

Centered text on top/bottom drawers, left-aligned on side drawers. Extends standard div props.

### DrawerFooter

Auto-positioned at bottom with `mt-auto`. Extends standard div props.

### DrawerTrigger / DrawerClose

Both support custom rendering via Radix's `asChild` pattern:

```tsx
<DrawerTrigger asChild>
  <Button>Open</Button>
</DrawerTrigger>
```

## Data Attributes

| Attribute | When Present | Applied To |
|-----------|--------------|------------|
| data-slot | Always | All components (for targeting) |
| data-vaul-drawer-direction | Always | DrawerContent (value: "top"\|"bottom"\|"left"\|"right") |
| data-open | Drawer open | DrawerOverlay (for animations) |
| data-closed | Drawer closed | DrawerOverlay (for animations) |

## Examples

### Basic Usage

```tsx
<Drawer>
  <DrawerTrigger asChild>
    <Button>Open Drawer</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Drawer Title</DrawerTitle>
      <DrawerDescription>This is a description.</DrawerDescription>
    </DrawerHeader>
    <div className="p-4">
      <p>Your content here.</p>
    </div>
  </DrawerContent>
</Drawer>
```

### Controlled State

```tsx
function ControlledDrawer() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button onClick={() => setOpen(true)}>Open</Button>
      <Drawer open={open} onOpenChange={setOpen}>
        <DrawerContent>
          <DrawerHeader>
            <DrawerTitle>Controlled Drawer</DrawerTitle>
          </DrawerHeader>
          <div className="p-4">
            <Button onClick={() => setOpen(false)}>Close Programmatically</Button>
          </div>
        </DrawerContent>
      </Drawer>
    </>
  )
}
```

### Settings Panel (Right Drawer)

```tsx
<Drawer direction="right">
  <DrawerTrigger asChild>
    <Button variant="outline">
      <SettingsIcon />
      Settings
    </Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Notification Settings</DrawerTitle>
      <DrawerDescription>Manage your notification preferences</DrawerDescription>
    </DrawerHeader>
    <div className="flex-1 space-y-4 overflow-y-auto p-4">
      <div className="flex items-center justify-between">
        <Label>Email Notifications</Label>
        <Switch />
      </div>
      <div className="flex items-center justify-between">
        <Label>Push Notifications</Label>
        <Switch />
      </div>
    </div>
    <DrawerFooter>
      <Button>Save Changes</Button>
      <DrawerClose asChild>
        <Button variant="outline">Cancel</Button>
      </DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>
```

### Mobile Navigation (Left Drawer)

```tsx
<Drawer direction="left">
  <DrawerTrigger asChild>
    <Button variant="ghost" size="icon">
      <MenuIcon />
    </Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Navigation</DrawerTitle>
    </DrawerHeader>
    <div className="flex-1 space-y-2 p-4">
      <Button variant="ghost" className="w-full justify-start">
        <HomeIcon />
        Dashboard
      </Button>
      <Button variant="ghost" className="w-full justify-start">
        <UserIcon />
        Profile
      </Button>
      <Button variant="ghost" className="w-full justify-start">
        <SettingsIcon />
        Settings
      </Button>
    </div>
  </DrawerContent>
</Drawer>
```

### Filter Panel (Bottom Drawer)

```tsx
<Drawer direction="bottom">
  <DrawerTrigger asChild>
    <Button variant="outline">Filters</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Filter Options</DrawerTitle>
      <DrawerDescription>Refine your results</DrawerDescription>
    </DrawerHeader>
    <div className="space-y-4 p-4">
      <div className="space-y-2">
        <Label htmlFor="start-date">Start Date</Label>
        <Input id="start-date" type="date" />
      </div>
      <div className="space-y-2">
        <Label htmlFor="category">Category</Label>
        <select id="category" className="...">
          <option>All</option>
          <option>Design</option>
          <option>Engineering</option>
        </select>
      </div>
    </div>
    <DrawerFooter>
      <div className="flex gap-2">
        <Button className="flex-1">Apply</Button>
        <Button variant="outline" className="flex-1">Reset</Button>
      </div>
    </DrawerFooter>
  </DrawerContent>
</Drawer>
```

### Non-Dismissible Drawer

```tsx
<Drawer dismissible={false}>
  <DrawerTrigger asChild>
    <Button>Important Action</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Confirm Action</DrawerTitle>
      <DrawerDescription>This action requires confirmation</DrawerDescription>
    </DrawerHeader>
    <div className="p-4">
      <p>You must explicitly confirm or cancel.</p>
    </div>
    <DrawerFooter>
      <Button>Confirm</Button>
      <DrawerClose asChild>
        <Button variant="outline">Cancel</Button>
      </DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>
```

### Custom Layout (No Header)

```tsx
<Drawer>
  <DrawerTrigger asChild>
    <Button>Custom Layout</Button>
  </DrawerTrigger>
  <DrawerContent>
    <div className="relative p-6">
      <DrawerClose asChild>
        <Button
          variant="ghost"
          size="icon-sm"
          className="absolute top-4 right-4"
        >
          <XIcon />
        </Button>
      </DrawerClose>
      <h2 className="text-lg font-semibold">Custom Layout</h2>
      <p className="mt-2 text-sm text-muted-foreground">
        Full control over structure
      </p>
    </div>
  </DrawerContent>
</Drawer>
```

## Keyboard

| Key | Action |
|-----|--------|
| Escape | Close drawer (if dismissible) |
| Tab | Navigate focusable elements |

## Accessibility

- Uses Vaul library built on Radix UI primitives for ARIA compliance
- `DrawerTitle` sets `aria-labelledby` on content
- `DrawerDescription` sets `aria-describedby` on content
- Focus trap when modal (default), focus returns to trigger on close
- Drag gestures announce state changes to screen readers
- Overlay has proper backdrop semantics

## Related

- [Dialog](/Users/bobringer/work/neynar/worktrees/frontend-monorepo/neyn-8162-nas-baseui-upgrade./dialog.llm.md) - Modal dialog for desktop-first interactions
- [Sheet](https://ui.shadcn.com/docs/components/sheet) - Similar side panel component (if available)
- [Popover](/Users/bobringer/work/neynar/worktrees/frontend-monorepo/neyn-8162-nas-baseui-upgrade./popover.llm.md) - Floating content without blocking interaction

---

# DropdownMenu

A versatile dropdown menu component for displaying contextual actions and selections.

## Import

```tsx
import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuGroup,
  DropdownMenuSub,
  DropdownMenuSubTrigger,
  DropdownMenuSubContent,
  DropdownMenuShortcut,
} from "@neynar/ui/dropdown-menu"
```

## Anatomy

```tsx
<DropdownMenu>
  <DropdownMenuTrigger>
    <Button>Open</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuLabel>Section</DropdownMenuLabel>
    <DropdownMenuSeparator />
    <DropdownMenuGroup>
      <DropdownMenuItem>Item</DropdownMenuItem>
    </DropdownMenuGroup>
  </DropdownMenuContent>
</DropdownMenu>
```

## Components

| Component | Description |
|-----------|-------------|
| DropdownMenu | Root container, manages open/close state and keyboard navigation |
| DropdownMenuTrigger | Button that opens the menu |
| DropdownMenuContent | Content container with automatic portal and positioning |
| DropdownMenuItem | Individual menu item with optional variants |
| DropdownMenuCheckboxItem | Menu item with checkbox for independent toggles |
| DropdownMenuRadioGroup | Container for mutually exclusive radio items |
| DropdownMenuRadioItem | Radio item for single selection within a group |
| DropdownMenuLabel | Label for grouping related items |
| DropdownMenuSeparator | Visual divider between menu sections |
| DropdownMenuGroup | Logical container for related items |
| DropdownMenuSub | Root container for nested submenus |
| DropdownMenuSubTrigger | Trigger for opening a submenu |
| DropdownMenuSubContent | Content container for submenu items |
| DropdownMenuShortcut | Visual display of keyboard shortcuts |

## Props

### DropdownMenu

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | - | Controlled open state |
| onOpenChange | (open: boolean) => void | - | Called when open state changes |
| defaultOpen | boolean | false | Uncontrolled initial open state |

### DropdownMenuTrigger

Uses `render` prop pattern for custom trigger elements:

```tsx
<DropdownMenuTrigger render={<Button variant="outline" />}>
  Open Menu
</DropdownMenuTrigger>
```

### DropdownMenuContent

Automatically renders in a portal with positioning system.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| align | "start" \| "center" \| "end" | "start" | Horizontal alignment relative to trigger |
| alignOffset | number | 0 | Offset in pixels from aligned position |
| side | "top" \| "right" \| "bottom" \| "left" | "bottom" | Preferred side to position menu |
| sideOffset | number | 4 | Distance in pixels from trigger |

### DropdownMenuItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "default" \| "destructive" \| "success" \| "warning" \| "info" | "default" | Visual style variant |
| inset | boolean | false | Add extra left padding for alignment |
| disabled | boolean | false | Disable item interaction |

### DropdownMenuCheckboxItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| checked | boolean \| "indeterminate" | - | Checked state |
| onCheckedChange | (checked: boolean) => void | - | Called when checked state changes |

### DropdownMenuRadioGroup

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | - | Currently selected value |
| onValueChange | (value: string) => void | - | Called when selection changes |

### DropdownMenuRadioItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | - | Value for this radio item |

### DropdownMenuLabel

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| inset | boolean | false | Add extra left padding for alignment with icon items |

### DropdownMenuSubTrigger

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| inset | boolean | false | Add extra left padding for alignment |

Automatically displays chevron icon on the right.

### DropdownMenuSubContent

Same props as DropdownMenuContent, but with different defaults:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| side | "top" \| "right" \| "bottom" \| "left" | "right" | Preferred side to position submenu |
| alignOffset | number | -3 | Offset for better visual alignment |

## Data Attributes (for styling)

| Attribute | When Present |
|-----------|--------------|
| data-open | Menu or submenu is open |
| data-closed | Menu or submenu is closed |
| data-highlighted | Item is keyboard-highlighted or hovered |
| data-disabled | Item is disabled |
| data-inset | Item or label has inset padding |

## Variants

DropdownMenuItem supports semantic variants:

| Variant | Use Case |
|---------|----------|
| default | Standard actions |
| destructive | Delete, remove, or dangerous actions |
| success | Confirmations or positive actions |
| warning | Caution-required actions |
| info | Informational actions |

## Examples

### Basic Menu

```tsx
<DropdownMenu>
  <DropdownMenuTrigger>
    <Button variant="outline">Open Menu</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuItem>
      <EditIcon />
      Edit
    </DropdownMenuItem>
    <DropdownMenuItem>
      <CopyIcon />
      Duplicate
    </DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem variant="destructive">
      <TrashIcon />
      Delete
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
```

### User Account Menu

```tsx
<DropdownMenu>
  <DropdownMenuTrigger>
    <Button variant="ghost" size="icon">
      <UserIcon />
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="end" className="w-56">
    <DropdownMenuLabel>My Account</DropdownMenuLabel>
    <DropdownMenuSeparator />
    <DropdownMenuItem>
      <UserIcon />
      Profile
      <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
    </DropdownMenuItem>
    <DropdownMenuItem>
      <SettingsIcon />
      Settings
      <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
    </DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem variant="destructive">
      <LogOutIcon />
      Log Out
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
```

### Checkbox Items

```tsx
function NotificationMenu() {
  const [notifications, setNotifications] = useState(true)
  const [marketing, setMarketing] = useState(false)

  return (
    <DropdownMenu>
      <DropdownMenuTrigger>
        <Button variant="outline">Preferences</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuLabel>Notifications</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuCheckboxItem
          checked={notifications}
          onCheckedChange={setNotifications}
        >
          <BellIcon />
          Push Notifications
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={marketing}
          onCheckedChange={setMarketing}
        >
          <MailIcon />
          Marketing Emails
        </DropdownMenuCheckboxItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Radio Group

```tsx
function ThemeSelector() {
  const [theme, setTheme] = useState("system")

  return (
    <DropdownMenu>
      <DropdownMenuTrigger>
        <Button variant="outline">Theme: {theme}</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuLabel>Theme</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
          <DropdownMenuRadioItem value="light">
            <SunIcon />
            Light
          </DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="dark">
            <MoonIcon />
            Dark
          </DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="system">
            <MonitorIcon />
            System
          </DropdownMenuRadioItem>
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Nested Submenus

```tsx
<DropdownMenu>
  <DropdownMenuTrigger>
    <Button variant="outline">Actions</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuItem>
      <EditIcon />
      Edit
    </DropdownMenuItem>
    <DropdownMenuItem>
      <CopyIcon />
      Duplicate
    </DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuSub>
      <DropdownMenuSubTrigger>
        <ShareIcon />
        Share
      </DropdownMenuSubTrigger>
      <DropdownMenuSubContent>
        <DropdownMenuItem>
          <MailIcon />
          Email
        </DropdownMenuItem>
        <DropdownMenuItem>
          <CopyIcon />
          Copy Link
        </DropdownMenuItem>
        <DropdownMenuItem>
          <DownloadIcon />
          Export
        </DropdownMenuItem>
      </DropdownMenuSubContent>
    </DropdownMenuSub>
    <DropdownMenuSeparator />
    <DropdownMenuItem variant="destructive">
      <TrashIcon />
      Delete
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
```

### Organized with Groups

```tsx
<DropdownMenu>
  <DropdownMenuTrigger>
    <Button variant="outline">Menu</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent className="w-56">
    <DropdownMenuGroup>
      <DropdownMenuLabel>My Account</DropdownMenuLabel>
    </DropdownMenuGroup>
    <DropdownMenuSeparator />
    <DropdownMenuGroup>
      <DropdownMenuItem>
        <UserIcon />
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem>
        <SettingsIcon />
        Settings
      </DropdownMenuItem>
    </DropdownMenuGroup>
    <DropdownMenuSeparator />
    <DropdownMenuGroup>
      <DropdownMenuLabel>Team</DropdownMenuLabel>
    </DropdownMenuGroup>
    <DropdownMenuGroup>
      <DropdownMenuItem>
        <PlusIcon />
        Invite Members
      </DropdownMenuItem>
      <DropdownMenuItem>
        <MailIcon />
        Team Settings
      </DropdownMenuItem>
    </DropdownMenuGroup>
  </DropdownMenuContent>
</DropdownMenu>
```

## Keyboard

| Key | Action |
|-----|--------|
| Space / Enter | Open menu (on trigger) / Activate item (in menu) |
| ArrowDown / ArrowUp | Navigate between items |
| ArrowRight | Open submenu |
| ArrowLeft | Close submenu |
| Escape | Close menu |
| Tab | Move focus out and close menu |

## Accessibility

- Built on Base UI Menu primitives with ARIA menu pattern
- Automatic focus management and keyboard navigation
- Screen reader announcements for menu state changes
- All items are properly labeled and keyboard accessible
- Checkbox and radio items announce their checked state
- Disabled items are properly marked with `aria-disabled`

## Related

- [ContextMenu](/components/context-menu) - Right-click menu with similar structure
- [Select](/components/select) - For form-based single selection
- [Combobox](/components/combobox) - For searchable selection
- [Button](/components/button) - Common trigger element

---

# Empty

Compound component for displaying empty states with media, messaging, and call-to-action elements.

## Import

```tsx
import {
  Empty,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
  EmptyDescription,
  EmptyContent,
} from "@neynar/ui/empty"
```

## Anatomy

```tsx
<Empty>
  <EmptyHeader>
    <EmptyMedia variant="icon">
      <Icon />
    </EmptyMedia>
    <EmptyTitle>Title</EmptyTitle>
    <EmptyDescription>Description text</EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button>Action</Button>
  </EmptyContent>
</Empty>
```

## Components

| Component | Description |
|-----------|-------------|
| Empty | Root container with centered flex layout and dashed border |
| EmptyHeader | Vertical stack for media, title, and description |
| EmptyMedia | Icon/media container with optional background styling |
| EmptyTitle | Large heading text |
| EmptyDescription | Supporting text with link styling |
| EmptyContent | Action area for buttons or links |

## Props

### Empty

Standard div props with centered layout and dashed border.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Empty state content |

### EmptyHeader

Standard div props. Groups media, title, and description.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Header content (media, title, description) |

### EmptyMedia

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "default" \| "icon" | "default" | Styling variant |
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Icon or media element |

**Variant Behavior:**
- `default`: Transparent background, use with custom-sized icons
- `icon`: Muted background with padding, auto-sizes icons to size-6

### EmptyTitle

Standard div props for the title heading.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Title text |

### EmptyDescription

Standard div props with link styling support.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Description text (supports inline links) |

Inline links (`<a>`) are automatically styled with underlines and hover effects.

### EmptyContent

Standard div props for action elements.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Action buttons or links |

## Data Attributes

All components have unique `data-slot` attributes for styling hooks:

| Attribute | Component |
|-----------|-----------|
| data-slot="empty" | Empty |
| data-slot="empty-header" | EmptyHeader |
| data-slot="empty-icon" | EmptyMedia |
| data-slot="empty-title" | EmptyTitle |
| data-slot="empty-description" | EmptyDescription |
| data-slot="empty-content" | EmptyContent |

EmptyMedia also includes:
- `data-variant`: Current variant value

## Variants

### Media Variants

| Variant | Use Case |
|---------|----------|
| default | Large custom-sized icons (size-12), transparent background |
| icon | Standard icons with muted background, auto-sized to size-6 |

## Examples

### Basic Empty State

```tsx
<Empty>
  <EmptyHeader>
    <EmptyMedia variant="icon">
      <SearchIcon />
    </EmptyMedia>
    <EmptyTitle>No results found</EmptyTitle>
    <EmptyDescription>
      Try adjusting your search terms or filters.
    </EmptyDescription>
  </EmptyHeader>
</Empty>
```

### With Action Button

```tsx
<Empty>
  <EmptyHeader>
    <EmptyMedia variant="icon">
      <WebhookIcon />
    </EmptyMedia>
    <EmptyTitle>No webhooks configured</EmptyTitle>
    <EmptyDescription>
      Get started by creating your first webhook to receive real-time notifications.
    </EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button>
      <WebhookIcon data-icon="inline-start" />
      Create Webhook
    </Button>
  </EmptyContent>
</Empty>
```

### With Multiple Actions

```tsx
<Empty>
  <EmptyHeader>
    <EmptyMedia variant="icon">
      <DatabaseIcon />
    </EmptyMedia>
    <EmptyTitle>No data sources</EmptyTitle>
    <EmptyDescription>
      Connect your first data source to start analyzing data.
    </EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <div className="flex flex-col gap-2 sm:flex-row">
      <Button>Connect Data Source</Button>
      <Button variant="outline">View Documentation</Button>
    </div>
  </EmptyContent>
</Empty>
```

### With Link in Description

```tsx
<Empty>
  <EmptyHeader>
    <EmptyMedia>
      <DatabaseIcon className="size-12 text-muted-foreground" />
    </EmptyMedia>
    <EmptyTitle>No data available</EmptyTitle>
    <EmptyDescription>
      Start collecting data by setting up your first integration.{" "}
      <a href="#">Learn more</a>
    </EmptyDescription>
  </EmptyHeader>
</Empty>
```

### Large Icon (Default Variant)

```tsx
<Empty>
  <EmptyHeader>
    <EmptyMedia variant="default">
      <FileTextIcon className="size-12 text-muted-foreground" />
    </EmptyMedia>
    <EmptyTitle>No documents</EmptyTitle>
    <EmptyDescription>
      Use the default variant with manually sized icons for larger displays.
    </EmptyDescription>
  </EmptyHeader>
</Empty>
```

### Minimal (No Media)

```tsx
<Empty>
  <EmptyHeader>
    <EmptyTitle>No items to display</EmptyTitle>
    <EmptyDescription>
      Empty states can work without media icons for simpler contexts.
    </EmptyDescription>
  </EmptyHeader>
</Empty>
```

### Success/Completed State

```tsx
<Empty>
  <EmptyHeader>
    <EmptyMedia variant="icon">
      <InboxIcon />
    </EmptyMedia>
    <EmptyTitle>All caught up!</EmptyTitle>
    <EmptyDescription>
      You have no new notifications. We'll notify you when something happens.
    </EmptyDescription>
  </EmptyHeader>
</Empty>
```

## Composition Patterns

### Minimal Pattern
Header with media and title only (no description, no actions).

### Standard Pattern
Header with media, title, and description (no actions).

### Action Pattern
Complete header + content section with one or more buttons.

### Text-Only Pattern
Header with title and description, no media or actions.

## Accessibility

- Use semantic HTML elements for text content
- Ensure sufficient color contrast for text and icons
- Keep empty state messages clear and actionable
- Provide keyboard-accessible actions when present
- Consider loading states vs empty states for better UX

## Related

- Button - For empty state actions
- Alert - For error or warning states
- Card - Often contains empty states

---

# Field

Comprehensive form field system with labels, descriptions, error handling, and flexible layouts for building accessible forms.

## Import

```tsx
import {
  Field,
  FieldLabel,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldSet,
  FieldLegend,
  FieldSeparator,
  FieldTitle,
} from "@neynar/ui/field"
```

## Anatomy

```tsx
<Field orientation="vertical" data-invalid={hasError}>
  <FieldLabel htmlFor="input-id">Label Text</FieldLabel>
  <FieldContent>
    <Input id="input-id" />
    <FieldDescription>Helper text</FieldDescription>
    <FieldError errors={errors} />
  </FieldContent>
</Field>
```

## Components

| Component | Description |
|-----------|-------------|
| Field | Base field wrapper with orientation support |
| FieldLabel | Label for the field input |
| FieldContent | Container for input + description + error |
| FieldDescription | Helper text providing context |
| FieldError | Error message display with auto-deduplication |
| FieldGroup | Container for multiple related fields |
| FieldSet | Semantic fieldset container for field groups |
| FieldLegend | Legend for FieldSet with variant styles |
| FieldSeparator | Visual separator between field sections |
| FieldTitle | Title text for checkbox/switch fields |

## Props

### Field

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| orientation | "vertical" \| "horizontal" \| "responsive" | "vertical" | Layout orientation |
| data-invalid | boolean | - | Apply error styling when true |
| data-disabled | boolean | - | Apply disabled styling when true |

### FieldLabel

Extends Label component. Use `htmlFor` to associate with input.

### FieldContent

Container with consistent spacing for input + description + error.

### FieldDescription

Helper text automatically styled for placement. Supports links with underline styling.

### FieldError

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| errors | Array<{ message?: string }> | - | Error objects to display |
| children | ReactNode | - | Custom error content |

Automatically deduplicates errors by message. Shows single error as text, multiple errors as bulleted list.

### FieldGroup

Container for multiple fields with consistent spacing. Provides `@container/field-group` for responsive layouts.

### FieldSet

Semantic `<fieldset>` element for grouping related fields. Use with FieldLegend.

### FieldLegend

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "legend" \| "label" | "legend" | Visual style variant |

### FieldSeparator

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | - | Optional label text on separator |

### FieldTitle

Title text for fields with embedded controls (checkboxes, switches). Use inside FieldLabel.

## Orientation Variants

| Variant | Behavior |
|---------|----------|
| vertical | Label and input stacked vertically (default) |
| horizontal | Label and input side by side |
| responsive | Vertical on mobile, horizontal on larger screens (requires FieldGroup) |

## Data Attributes

| Attribute | When Present | Used For |
|-----------|--------------|----------|
| data-invalid | Field has errors | Error styling |
| data-disabled | Field is disabled | Disabled styling |
| data-orientation | Always | Current orientation value |
| data-slot | Always | Component identification |

## Examples

### Basic Field

```tsx
<Field>
  <FieldLabel htmlFor="username">Username</FieldLabel>
  <Input id="username" placeholder="Enter username" />
</Field>
```

### Field with Description

```tsx
<Field>
  <FieldLabel htmlFor="email">Email Address</FieldLabel>
  <FieldContent>
    <Input id="email" type="email" placeholder="your@email.com" />
    <FieldDescription>We'll never share your email</FieldDescription>
  </FieldContent>
</Field>
```

### Field with Error State

```tsx
const [errors, setErrors] = useState({ email: { message: "Invalid email" } })

<Field data-invalid={!!errors.email}>
  <FieldLabel htmlFor="email">Email *</FieldLabel>
  <FieldContent>
    <Input
      id="email"
      type="email"
      aria-invalid={!!errors.email}
    />
    <FieldError errors={[errors.email]} />
  </FieldContent>
</Field>
```

### Horizontal Layout with Switch

```tsx
<Field orientation="horizontal">
  <FieldLabel htmlFor="notifications">
    <Switch id="notifications" />
    <div>
      <FieldTitle>Enable Notifications</FieldTitle>
      <FieldDescription>Receive email updates</FieldDescription>
    </div>
  </FieldLabel>
</Field>
```

### Grouped Fields with Separator

```tsx
<FieldGroup>
  <Field>
    <FieldLabel htmlFor="name">Name</FieldLabel>
    <Input id="name" />
  </Field>

  <FieldSeparator>Security Settings</FieldSeparator>

  <Field>
    <FieldLabel htmlFor="password">Password</FieldLabel>
    <Input id="password" type="password" />
  </Field>
</FieldGroup>
```

### Field Set with Radio Group

```tsx
<FieldSet>
  <FieldLegend variant="label">Event Type</FieldLegend>
  <FieldGroup>
    <RadioGroup value={value} onValueChange={setValue}>
      <Field orientation="horizontal">
        <FieldLabel htmlFor="option-1">
          <RadioGroupItem value="option1" id="option-1" />
          <div>
            <FieldTitle>Option One</FieldTitle>
            <FieldDescription>First choice description</FieldDescription>
          </div>
        </FieldLabel>
      </Field>

      <Field orientation="horizontal">
        <FieldLabel htmlFor="option-2">
          <RadioGroupItem value="option2" id="option-2" />
          <div>
            <FieldTitle>Option Two</FieldTitle>
            <FieldDescription>Second choice description</FieldDescription>
          </div>
        </FieldLabel>
      </Field>
    </RadioGroup>
  </FieldGroup>
</FieldSet>
```

### Multiple Errors

```tsx
<Field data-invalid={true}>
  <FieldLabel htmlFor="password">Password</FieldLabel>
  <FieldContent>
    <Input id="password" type="password" aria-invalid={true} />
    <FieldError
      errors={[
        { message: "Must be at least 8 characters" },
        { message: "Must contain a number" },
        { message: "Must contain a special character" },
      ]}
    />
  </FieldContent>
</Field>
```

### Responsive Layout

```tsx
<FieldGroup>
  <Field orientation="responsive">
    <FieldLabel htmlFor="phone">Phone Number</FieldLabel>
    <FieldContent>
      <Input id="phone" placeholder="+1 (555) 123-4567" />
      <FieldDescription>
        Vertical on mobile, horizontal on larger screens
      </FieldDescription>
    </FieldContent>
  </Field>
</FieldGroup>
```

### Disabled Field

```tsx
<Field data-disabled={true}>
  <FieldLabel htmlFor="readonly">Read Only Field</FieldLabel>
  <FieldContent>
    <Input
      id="readonly"
      disabled
      defaultValue="Cannot edit"
    />
    <FieldDescription>This field cannot be modified</FieldDescription>
  </FieldContent>
</Field>
```

## Accessibility

- Uses semantic HTML elements (`fieldset`, `legend`, `label`)
- FieldError uses `role="alert"` for screen reader announcements
- Supports `aria-invalid` on inputs for error states
- FieldLabel properly associates with inputs via `htmlFor`
- FieldDescription provides additional context without cluttering labels
- Disabled state reduces opacity for visual indication

## Best Practices

- Always use `htmlFor` on FieldLabel to associate with input `id`
- Set `data-invalid` on Field and `aria-invalid` on input for error states
- Use FieldContent when you have description or error messages
- For checkbox/switch fields, use horizontal orientation with FieldTitle
- Use FieldSet/FieldLegend for semantically related field groups
- Use FieldGroup for visual grouping without semantic fieldset
- Set `data-disabled` on Field when input is disabled
- Use FieldSeparator to organize long forms into sections

## Related

- [Input](./input.llm.md) - Text input component
- [Textarea](./textarea.llm.md) - Multi-line text input
- [Checkbox](./checkbox.llm.md) - Checkbox input
- [Switch](./switch.llm.md) - Toggle switch
- [Radio Group](./radio-group.llm.md) - Radio button group
- [Label](./label.llm.md) - Base label component

---

# HoverCard

Displays rich preview content when hovering over a trigger element.

## Import

```tsx
import { HoverCard, HoverCardTrigger, HoverCardContent } from "@neynar/ui/hover-card"
```

## Anatomy

```tsx
<HoverCard>
  <HoverCardTrigger>
    <a href="#">Hover over me</a>
  </HoverCardTrigger>
  <HoverCardContent>
    Preview content appears here
  </HoverCardContent>
</HoverCard>
```

## Components

| Component | Description |
|-----------|-------------|
| HoverCard | Root container, manages hover state and timing |
| HoverCardTrigger | Element that triggers the hover card (link, button, text, avatar) |
| HoverCardContent | Content container with automatic portal, positioning, and animations |

## Props

### HoverCard

Inherits all props from `@base-ui/react/preview-card` Root component.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | - | Controlled open state |
| defaultOpen | boolean | false | Uncontrolled initial open state |
| onOpenChange | (open: boolean) => void | - | Called when open state changes |

### HoverCardTrigger

Inherits all props from `@base-ui/react/preview-card` Trigger component.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| render | ReactElement \| function | - | Custom element or render function for the trigger |

The `render` prop accepts a ReactElement or function for customization:

```tsx
// As a ReactElement
<HoverCardTrigger render={<Button variant="outline">Hover</Button>}>
  Content
</HoverCardTrigger>

// As a function
<HoverCardTrigger render={(props) => <a {...props} href="#">Link</a>}>
  Content
</HoverCardTrigger>
```

### HoverCardContent

Automatically renders portal and positioner. Combines props from Popup and Positioner components.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| side | "top" \| "right" \| "bottom" \| "left" | "bottom" | Which side of trigger to position the card |
| sideOffset | number | 4 | Distance from trigger in pixels |
| align | "start" \| "center" \| "end" | "center" | How to align relative to the trigger |
| alignOffset | number | 4 | Additional alignment axis offset in pixels |
| className | string | - | Custom CSS classes (merged with defaults) |
| initialFocus | boolean \| RefObject \| function | - | Element to focus when opened |
| finalFocus | boolean \| RefObject \| function | - | Element to focus when closed |

## Data Attributes

Use these for custom styling and animations:

| Attribute | When Present | Description |
|-----------|--------------|-------------|
| data-open | Card is open | Apply open state styles |
| data-closed | Card is closed | Apply closed state styles |
| data-side | Always | Value: "top" \| "right" \| "bottom" \| "left" |
| data-align | Always | Value: "start" \| "center" \| "end" |
| data-starting-style | Opening animation | Initial animation state |
| data-ending-style | Closing animation | Final animation state |

## Examples

### User Profile Preview

```tsx
<HoverCard>
  <HoverCardTrigger>
    <button className="inline-flex items-center gap-2">
      <Avatar size="sm">
        <AvatarImage src="/user.jpg" />
        <AvatarFallback>DR</AvatarFallback>
      </Avatar>
      <span className="font-medium">@dwr</span>
    </button>
  </HoverCardTrigger>
  <HoverCardContent className="w-80">
    <div className="space-y-3">
      <div className="flex items-start justify-between">
        <div className="flex items-center gap-3">
          <Avatar size="lg">
            <AvatarImage src="/user.jpg" />
            <AvatarFallback>DR</AvatarFallback>
          </Avatar>
          <div>
            <p className="font-semibold">Dan Romero</p>
            <p className="text-muted-foreground text-sm">@dwr</p>
          </div>
        </div>
        <Button size="sm">Follow</Button>
      </div>
      <p className="text-sm">
        Building Farcaster and Warpcast. Previously VP Eng at Coinbase.
      </p>
      <div className="flex items-center gap-4 text-sm">
        <div>
          <span className="font-semibold">124.5K</span>{" "}
          <span className="text-muted-foreground">followers</span>
        </div>
        <div>
          <span className="font-semibold">428</span>{" "}
          <span className="text-muted-foreground">following</span>
        </div>
      </div>
    </div>
  </HoverCardContent>
</HoverCard>
```

### Simple Text Preview

```tsx
<p>
  Learn more about{" "}
  <HoverCard>
    <HoverCardTrigger>
      <a href="#" className="text-primary hover:underline">
        typography
      </a>
    </HoverCardTrigger>
    <HoverCardContent>
      <p className="text-sm">
        Typography is the art and science of arranging type to make written
        language clear, visually appealing, and effective.
      </p>
    </HoverCardContent>
  </HoverCard>
  {" "}in design.
</p>
```

### Positioned Above with Custom Width

```tsx
<HoverCard>
  <HoverCardTrigger>
    <Badge variant="secondary" className="cursor-pointer">
      React 19
    </Badge>
  </HoverCardTrigger>
  <HoverCardContent side="top" className="w-96">
    <div className="space-y-2">
      <h4 className="text-sm font-semibold">React 19</h4>
      <p className="text-muted-foreground text-xs leading-relaxed">
        Latest version of React with improved concurrent rendering,
        automatic batching, and new hooks for better performance.
      </p>
    </div>
  </HoverCardContent>
</HoverCard>
```

### Rich Content with Avatar and Actions

```tsx
<HoverCard>
  <HoverCardTrigger>
    <Button variant="outline">Farcaster Protocol</Button>
  </HoverCardTrigger>
  <HoverCardContent className="w-72">
    <div className="space-y-3">
      <div className="flex items-start gap-3">
        <Avatar size="sm">
          <AvatarFallback>FC</AvatarFallback>
        </Avatar>
        <div className="flex-1 space-y-1">
          <h4 className="text-sm font-semibold">Farcaster Protocol</h4>
          <p className="text-muted-foreground text-xs">
            A sufficiently decentralized social network.
          </p>
        </div>
      </div>
      <div className="flex gap-2">
        <Button size="xs" variant="secondary">Learn More</Button>
        <Button size="xs" variant="outline">Documentation</Button>
      </div>
    </div>
  </HoverCardContent>
</HoverCard>
```

### Multiple Triggers in Feed

```tsx
<div className="space-y-4">
  {posts.map((post) => (
    <div key={post.id} className="border rounded-lg p-4">
      <div className="mb-2 flex items-center gap-2">
        <HoverCard>
          <HoverCardTrigger>
            <button className="inline-flex items-center gap-2">
              <Avatar size="sm">
                <AvatarImage src={post.author.avatar} />
                <AvatarFallback>{post.author.initials}</AvatarFallback>
              </Avatar>
              <span className="font-medium hover:underline">
                @{post.author.username}
              </span>
            </button>
          </HoverCardTrigger>
          <HoverCardContent className="w-80">
            <UserProfilePreview user={post.author} />
          </HoverCardContent>
        </HoverCard>
        <span className="text-muted-foreground text-sm">{post.time}</span>
      </div>
      <p className="text-sm">{post.content}</p>
    </div>
  ))}
</div>
```

## Keyboard

HoverCard is primarily mouse/touch interaction, but keyboard navigation is supported:

| Key | Action |
|-----|--------|
| Hover | Open hover card after delay |
| Mouse leave | Close hover card after delay |
| Tab | Navigate through interactive elements inside content |
| Escape | Close hover card immediately |

## Accessibility

- Automatically manages focus when opening/closing
- Content is rendered in a portal to avoid z-index issues
- Uses ARIA attributes for screen reader support
- Supports keyboard navigation for interactive content
- Trigger remains accessible and keyboard-navigable
- Respects user's motion preferences for animations

## Styling Notes

- Default width is `w-64` (256px), customize with `className`
- Uses frosted glass effect with `bg-popover` at 75% opacity and backdrop blur
- Includes entry/exit animations via data attributes
- Side-specific slide animations (slides from opposite direction)
- Respects `--transform-origin` CSS variable for proper animation origins
- Compatible with all themes (classic, frosted, sketch)

## Related

- [Tooltip](/components/tooltip.llm.md) - For simple text hints (non-interactive)
- [Popover](/components/popover.llm.md) - For click-triggered interactive content
- [Dialog](/components/dialog.llm.md) - For modal overlays
- [Avatar](/components/avatar.llm.md) - Commonly used in hover card triggers

---

# InputGroup

Compound component for adding icons, text, buttons, or keyboard shortcuts to inputs and textareas.

## Import

```tsx
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
  InputGroupText,
  InputGroupTextarea,
} from "@neynar/ui/input-group"
```

## Anatomy

```tsx
<InputGroup>
  <InputGroupAddon align="inline-start">
    <SearchIcon />
  </InputGroupAddon>
  <InputGroupInput placeholder="Search..." />
  <InputGroupAddon align="inline-end">
    <InputGroupButton size="icon-xs">
      <XIcon />
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>
```

## Components

| Component | Description |
|-----------|-------------|
| InputGroup | Root container that manages unified border, focus ring, and validation states |
| InputGroupInput | Input element without its own border/ring (integrated with group) |
| InputGroupTextarea | Textarea element without its own border/ring (integrated with group) |
| InputGroupAddon | Container for addon content (icons, text, buttons) at start/end |
| InputGroupButton | Button sized for use within addons |
| InputGroupText | Text label/prefix/suffix for addons (e.g., "https://", "USD") |

## Props

### InputGroup

Root container. Extends standard `<div>` props.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| data-disabled | boolean | - | Disables addon opacity when set to "true" |

### InputGroupAddon

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| align | "inline-start" \| "inline-end" \| "block-start" \| "block-end" | "inline-start" | Position of addon relative to input |
| className | string | - | Additional CSS classes |

Clicking the addon automatically focuses the input (unless clicking a button inside).

### InputGroupButton

Extends Button component props with custom sizes.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| size | "xs" \| "sm" \| "icon-xs" \| "icon-sm" | "xs" | Button size optimized for input groups |
| variant | ButtonVariant | "ghost" | Button visual style |
| type | "button" \| "submit" \| "reset" | "button" | HTML button type |
| className | string | - | Additional CSS classes |

### InputGroupText

Extends standard `<span>` props. Use for text prefixes/suffixes like "https://", "@", "/", "USD", "MB".

### InputGroupInput

Extends standard `<input>` props. Inherits all Input component functionality but without its own border/focus ring.

### InputGroupTextarea

Extends standard `<textarea>` props. Inherits all Textarea component functionality but without its own border/focus ring.

## Alignment Options

| Align | Position | Use Case |
|-------|----------|----------|
| inline-start | Left side (horizontal) | Icons, text prefixes, leading buttons |
| inline-end | Right side (horizontal) | Icons, text suffixes, action buttons |
| block-start | Top (vertical) | Labels above input |
| block-end | Bottom (vertical) | Help text or actions below input |

## Button Sizes

| Size | Dimensions | Use Case |
|------|------------|----------|
| xs | h-6 (24px) | Text buttons with labels |
| sm | Standard height | Larger text buttons |
| icon-xs | 24x24px | Small icon-only buttons |
| icon-sm | 32x32px | Medium icon-only buttons |

## Data Attributes

| Attribute | When Present |
|-----------|--------------|
| data-slot="input-group" | On root container |
| data-slot="input-group-addon" | On addon containers |
| data-slot="input-group-control" | On input/textarea |
| data-align | Shows alignment value on addons |
| data-disabled="true" | When group is disabled (reduces addon opacity) |
| aria-invalid="true" | When input is invalid (shows destructive ring) |

## Examples

### Icon Addons

```tsx
// Icon at start
<InputGroup>
  <InputGroupAddon align="inline-start">
    <SearchIcon />
  </InputGroupAddon>
  <InputGroupInput placeholder="Search..." />
</InputGroup>

// Icon at end
<InputGroup>
  <InputGroupInput placeholder="Select date..." />
  <InputGroupAddon align="inline-end">
    <CalendarIcon />
  </InputGroupAddon>
</InputGroup>

// Both sides
<InputGroup>
  <InputGroupAddon align="inline-start">
    <SearchIcon />
  </InputGroupAddon>
  <InputGroupInput placeholder="Search..." />
  <InputGroupAddon align="inline-end">
    <XIcon />
  </InputGroupAddon>
</InputGroup>
```

### Text Addons

```tsx
// URL prefix
<InputGroup>
  <InputGroupAddon align="inline-start">
    <InputGroupText>https://</InputGroupText>
  </InputGroupAddon>
  <InputGroupInput placeholder="example.com" />
</InputGroup>

// Currency suffix
<InputGroup>
  <InputGroupInput type="number" placeholder="0.00" />
  <InputGroupAddon align="inline-end">
    <InputGroupText>USD</InputGroupText>
  </InputGroupAddon>
</InputGroup>

// Username format
<InputGroup>
  <InputGroupAddon align="inline-start">
    <InputGroupText>@</InputGroupText>
  </InputGroupAddon>
  <InputGroupInput placeholder="username" />
  <InputGroupAddon align="inline-end">
    <InputGroupText>.eth</InputGroupText>
  </InputGroupAddon>
</InputGroup>
```

### Button Addons

```tsx
// Clear button
<InputGroup>
  <InputGroupAddon align="inline-start">
    <SearchIcon />
  </InputGroupAddon>
  <InputGroupInput placeholder="Search..." value={query} />
  <InputGroupAddon align="inline-end">
    <InputGroupButton size="icon-xs" aria-label="Clear" onClick={handleClear}>
      <XIcon />
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>

// Copy button
<InputGroup>
  <InputGroupInput value="neynar_sk_prod_abc123" readOnly className="font-mono text-sm" />
  <InputGroupAddon align="inline-end">
    <InputGroupButton size="icon-xs" aria-label="Copy" onClick={handleCopy}>
      {copied ? <CheckIcon /> : <CopyIcon />}
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>

// Action button with text
<InputGroup>
  <InputGroupInput placeholder="Enter email..." />
  <InputGroupAddon align="inline-end">
    <InputGroupButton size="xs">Send</InputGroupButton>
  </InputGroupAddon>
</InputGroup>
```

### Multiple Buttons

```tsx
// Password with visibility toggle and copy
<InputGroup>
  <InputGroupAddon align="inline-start">
    <LockIcon />
  </InputGroupAddon>
  <InputGroupInput type={showPassword ? "text" : "password"} placeholder="••••••••" />
  <InputGroupAddon align="inline-end">
    <InputGroupButton size="icon-xs" aria-label="Toggle visibility" onClick={toggleVisibility}>
      {showPassword ? <EyeOffIcon /> : <EyeIcon />}
    </InputGroupButton>
    <InputGroupButton size="icon-xs" aria-label="Copy" onClick={handleCopy}>
      <CopyIcon />
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>
```

### Complex Combinations

```tsx
// API endpoint builder with multiple addons
<InputGroup>
  <InputGroupAddon align="inline-start">
    <InputGroupText>https://api.neynar.com/</InputGroupText>
  </InputGroupAddon>
  <InputGroupInput placeholder="v2/cast/conversation" />
  <InputGroupAddon align="inline-end">
    <InputGroupButton size="xs" variant="outline">
      Test
    </InputGroupButton>
    <InputGroupButton size="icon-xs" aria-label="Copy">
      <CopyIcon />
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>
```

### States

```tsx
// Disabled
<InputGroup data-disabled="true">
  <InputGroupAddon align="inline-start">
    <SearchIcon />
  </InputGroupAddon>
  <InputGroupInput placeholder="Disabled..." disabled />
</InputGroup>

// Invalid/Error
<InputGroup>
  <InputGroupAddon align="inline-start">
    <SearchIcon />
  </InputGroupAddon>
  <InputGroupInput placeholder="Invalid input" aria-invalid="true" />
</InputGroup>

// Loading
<InputGroup>
  <InputGroupAddon align="inline-start">
    <Loader2Icon className="animate-spin" />
  </InputGroupAddon>
  <InputGroupInput placeholder="Loading..." />
</InputGroup>

// Read-only
<InputGroup>
  <InputGroupAddon align="inline-start">
    <LockIcon />
  </InputGroupAddon>
  <InputGroupInput value="neynar_sk_readonly" readOnly className="font-mono text-sm" />
  <InputGroupAddon align="inline-end">
    <InputGroupButton size="icon-xs" aria-label="Copy">
      <CopyIcon />
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>
```

## Keyboard

| Key | Action |
|-----|--------|
| Tab | Move focus to/from input |
| Click addon | Focus input (unless clicking button) |

Input and button keyboard interactions follow standard HTML behavior.

## Accessibility

- InputGroup uses `role="group"` for semantic grouping
- Clicking addon automatically focuses input for better UX
- Icon-only buttons require `aria-label` for screen readers
- Invalid state shows destructive border/ring when `aria-invalid="true"` is set
- Disabled state reduces addon opacity and disables interactive elements
- Focus ring appears on the entire group container, not individual inputs

## Styling Notes

### Icon Auto-Sizing

Icons within addons are automatically sized to 16px (`size-4`) unless a custom size class is applied.

### Focus Management

The InputGroup container manages the focus ring. Individual inputs/textareas have their borders and rings removed to integrate seamlessly.

### Validation States

Set `aria-invalid="true"` on InputGroupInput to show destructive border and ring on the entire group.

## Related

- **Input** - Standalone input without addons
- **Textarea** - Standalone textarea
- **Field** - Wrap InputGroup with label and description
- **Button** - Used within InputGroupButton

---

# InputOTP

One-time password input with individual character slots for verification codes, 2FA, and authentication flows.

## Import

```tsx
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
  InputOTPSeparator,
} from "@neynar/ui/input-otp"
```

## Anatomy

```tsx
<InputOTP maxLength={6} value={code} onChange={setCode}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
  </InputOTPGroup>
  <InputOTPSeparator />
  <InputOTPGroup>
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>
```

## Components

| Component | Description |
|-----------|-------------|
| InputOTP | Root container managing OTP input state and behavior |
| InputOTPGroup | Groups multiple slots together, supports error styling |
| InputOTPSlot | Individual character slot displaying current value |
| InputOTPSeparator | Visual separator between groups (minus icon) |

## Props

### InputOTP

Built on the `input-otp` library. Accepts all standard input props plus:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| maxLength | number | - | Number of character slots (required) |
| value | string | - | Controlled value state |
| onChange | (value: string) => void | - | Called when value changes |
| pattern | string | - | Regex pattern to validate input (e.g., `^[0-9]+$`) |
| disabled | boolean | false | Disabled state |
| autoFocus | boolean | false | Auto-focus on mount |
| containerClassName | string | - | Additional classes for container |
| onComplete | () => void | - | Called when all slots are filled |
| inputMode | string | 'numeric' | Virtual keyboard type on mobile |
| pasteTransformer | (text: string) => string | - | Transform pasted text before applying |

### InputOTPGroup

Container for grouping slots. Set `aria-invalid={true}` to show error state.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| aria-invalid | boolean | - | Shows error styling when true |
| className | string | - | Additional classes |

### InputOTPSlot

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| index | number | - | Zero-based slot position (required) |
| className | string | - | Additional classes |

Automatically displays character from context, active state, and animated caret.

### InputOTPSeparator

Visual separator with minus icon. Standard div props accepted.

## Data Attributes

### InputOTP

| Attribute | When Present |
|-----------|--------------|
| data-slot | Always "input-otp" |

### InputOTPSlot

| Attribute | When Present |
|-----------|--------------|
| data-slot | Always "input-otp-slot" |
| data-active | Slot is currently focused |

## Examples

### Basic 6-Digit Code

```tsx
import { useState } from "react"
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
  InputOTPSeparator,
} from "@neynar/ui/input-otp"

function TwoFactorAuth() {
  const [code, setCode] = useState("")

  return (
    <InputOTP maxLength={6} value={code} onChange={setCode}>
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}
```

### Email Verification with Error State

```tsx
import { useState } from "react"
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
  InputOTPSeparator,
} from "@neynar/ui/input-otp"
import { Label } from "@neynar/ui/label"
import { Button } from "@neynar/ui/button"

function EmailVerification() {
  const [code, setCode] = useState("")
  const [error, setError] = useState(false)

  const handleVerify = () => {
    if (code !== "123456") {
      setError(true)
    }
  }

  return (
    <div className="space-y-3">
      <Label htmlFor="email-code">
        Enter the code sent to your email
      </Label>
      <InputOTP
        id="email-code"
        maxLength={6}
        value={code}
        onChange={setCode}
        pattern="^[0-9]+$"
      >
        <InputOTPGroup aria-invalid={error}>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup aria-invalid={error}>
          <InputOTPSlot index={3} />
          <InputOTPSlot index={4} />
          <InputOTPSlot index={5} />
        </InputOTPGroup>
      </InputOTP>
      {error && (
        <p className="text-destructive text-sm">
          Invalid code. Please try again.
        </p>
      )}
      <Button onClick={handleVerify} disabled={code.length !== 6}>
        Verify
      </Button>
    </div>
  )
}
```

### 4-Digit PIN

```tsx
<InputOTP maxLength={4}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
  </InputOTPGroup>
</InputOTP>
```

### 8-Digit Backup Code

```tsx
<InputOTP maxLength={8}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
  </InputOTPGroup>
  <InputOTPSeparator />
  <InputOTPGroup>
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
    <InputOTPSlot index={6} />
    <InputOTPSlot index={7} />
  </InputOTPGroup>
</InputOTP>
```

### With Paste Transformer

```tsx
<InputOTP
  maxLength={6}
  pattern="^[0-9]+$"
  pasteTransformer={(text) => text.replaceAll("-", "")}
>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>
```

Now users can paste "123-456" and it becomes "123456".

## Keyboard

| Key | Action |
|-----|--------|
| 0-9, A-Z | Enter character (if allowed by pattern) |
| Backspace | Delete previous character |
| Delete | Clear current slot |
| ArrowLeft | Move to previous slot |
| ArrowRight | Move to next slot |
| Cmd/Ctrl+V | Paste full code (transformed if pasteTransformer provided) |

## Accessibility

- Uses native input element for proper screen reader support
- Announces current slot and total slots to assistive technology
- Focus management handles keyboard navigation between slots
- Supports autocomplete="one-time-code" for iOS/Android SMS autofill
- Error state communicated via aria-invalid on groups

## Related

- [Label](./label.llm.md) - Form labels
- [Button](./button.llm.md) - Verification actions
- [Form](./form.llm.md) - Form integration

---

# Input

Text input field with support for all HTML input types.

## Import

```tsx
import { Input } from "@neynar/ui/input"
```

## Anatomy

```tsx
<Input type="text" placeholder="Enter text..." />
```

## Props

All standard HTML input attributes are supported via `React.ComponentProps<"input">`:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| type | string | "text" | HTML input type (text, email, password, number, tel, url, search, date, time, file, etc.) |
| placeholder | string | - | Placeholder text |
| value | string | - | Controlled input value |
| defaultValue | string | - | Uncontrolled default value |
| disabled | boolean | false | Disable the input |
| readOnly | boolean | false | Make input read-only |
| required | boolean | false | Mark as required field |
| aria-invalid | boolean | false | Show validation error styling |
| className | string | - | Additional CSS classes |

### Validation Styling

Set `aria-invalid="true"` to show error state with red border and ring:

```tsx
<Input aria-invalid="true" />
```

## Data Attributes

| Attribute | When Present |
|-----------|--------------|
| data-slot | Always "input" for styling hooks |

## Examples

### Basic Text Input

```tsx
<div className="space-y-2">
  <Label htmlFor="name">Name</Label>
  <Input id="name" placeholder="Enter your name" />
</div>
```

### Email Input with Validation

```tsx
function EmailInput() {
  const [email, setEmail] = useState("")
  const [error, setError] = useState(false)

  const handleBlur = () => {
    if (email && !email.includes("@")) {
      setError(true)
    } else {
      setError(false)
    }
  }

  return (
    <div className="space-y-2">
      <Label htmlFor="email">Email</Label>
      <Input
        id="email"
        type="email"
        placeholder="your@email.com"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        onBlur={handleBlur}
        aria-invalid={error}
      />
      {error && (
        <p className="text-sm text-destructive">
          Please enter a valid email address
        </p>
      )}
    </div>
  )
}
```

### Number Input

```tsx
<div className="space-y-2">
  <Label htmlFor="engagement">Min Engagement</Label>
  <Input
    id="engagement"
    type="number"
    placeholder="e.g., 100"
    min="0"
  />
</div>
```

### Password Input

```tsx
<div className="space-y-2">
  <Label htmlFor="password">Password</Label>
  <Input
    id="password"
    type="password"
    placeholder="••••••••"
  />
</div>
```

### Date Input

```tsx
<div className="space-y-2">
  <Label htmlFor="start-date">Start Date</Label>
  <Input
    id="start-date"
    type="date"
  />
</div>
```

### File Upload

```tsx
<div className="space-y-2">
  <Label htmlFor="file">Upload File</Label>
  <Input
    id="file"
    type="file"
    accept="image/*"
    multiple
  />
</div>
```

### Disabled State

```tsx
<Input placeholder="Disabled input" disabled />
```

### Read Only State

```tsx
<Input defaultValue="Read only value" readOnly />
```

### With Helper Text

```tsx
<div className="space-y-2">
  <Label htmlFor="username">Username</Label>
  <Input id="username" placeholder="Enter username" />
  <p className="text-sm text-muted-foreground">
    Choose a unique username
  </p>
</div>
```

## Keyboard

Standard HTML input keyboard behavior:

| Key | Action |
|-----|--------|
| Tab | Move focus to next field |
| Shift+Tab | Move focus to previous field |
| Enter | Submit form (if in form) |
| Escape | Clear value (browser default) |

## Accessibility

- Wraps Base UI `Input` component with full keyboard and screen reader support
- Use with `Label` component for proper associations via `htmlFor` and `id`
- Set `aria-invalid="true"` for validation errors
- Use `required` attribute for required fields
- Include helper text with validation messages below the input

## Related

- [InputGroup](./input-group.llm.md) - For inputs with icons, buttons, or text addons
- [Label](./label.llm.md) - For accessible input labels
- [Textarea](./textarea.llm.md) - For multi-line text input
- [Select](./select.llm.md) - For dropdown selection
- [Combobox](./combobox.llm.md) - For searchable select with autocomplete

---

# Item

Flexible compound component system for building structured list items with media, content, and actions.

## Import

```tsx
import {
  Item,
  ItemGroup,
  ItemSeparator,
  ItemMedia,
  ItemContent,
  ItemTitle,
  ItemDescription,
  ItemActions,
  ItemHeader,
  ItemFooter,
} from "@neynar/ui/item"
```

## Anatomy

```tsx
<ItemGroup>
  <Item>
    <ItemMedia variant="icon">
      <Icon />
    </ItemMedia>
    <ItemContent>
      <ItemTitle>Title</ItemTitle>
      <ItemDescription>Description text</ItemDescription>
    </ItemContent>
    <ItemActions>
      <Button />
    </ItemActions>
  </Item>
</ItemGroup>
```

## Components

| Component | Description |
|-----------|-------------|
| ItemGroup | Container for organizing related items in a vertical list |
| Item | Root item component, supports `render` prop for custom elements |
| ItemSeparator | Horizontal divider between items |
| ItemMedia | Container for icons, images, or avatars |
| ItemContent | Main content area that flexes to fill space |
| ItemTitle | Medium-weight title text with badge support |
| ItemDescription | Secondary description text (truncates at 2 lines) |
| ItemActions | Container for buttons or controls |
| ItemHeader | Full-width header for complex layouts |
| ItemFooter | Full-width footer for additional info |

## Props

### Item

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "default" \| "outline" \| "muted" | "default" | Visual style variant |
| size | "default" \| "sm" \| "xs" | "default" | Size affecting padding and spacing |
| render | ReactNode | - | Custom element to render as (e.g., `<a>`, `<button>`) |

### ItemMedia

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "default" \| "icon" \| "image" | "default" | Media type determining sizing and layout |

**Behavior**: Automatically aligns to top when `ItemDescription` is present using CSS container queries.

### ItemGroup

Container with `role="list"` that automatically adjusts gap based on child item sizes:
- `size="default"`: gap-4
- `size="sm"`: gap-2.5
- `size="xs"`: gap-2

### render Prop (Item)

Customize the underlying HTML element:

```tsx
// Render as link
<Item render={<a href="/profile" />}>
  <ItemContent>
    <ItemTitle>View Profile</ItemTitle>
  </ItemContent>
</Item>

// Render as button
<Item render={<button onClick={handleClick} />}>
  <ItemContent>
    <ItemTitle>Clickable Item</ItemTitle>
  </ItemContent>
</Item>
```

## Data Attributes

| Attribute | When Present | Used By |
|-----------|--------------|---------|
| data-slot="item" | Always | Item component |
| data-slot="item-media" | Always | ItemMedia component |
| data-slot="item-content" | Always | ItemContent component |
| data-slot="item-title" | Always | ItemTitle component |
| data-slot="item-description" | Always | ItemDescription component |
| data-slot="item-actions" | Always | ItemActions component |
| data-slot="item-header" | Always | ItemHeader component |
| data-slot="item-footer" | Always | ItemFooter component |
| data-slot="item-separator" | Always | ItemSeparator component |
| data-variant | Always | ItemMedia (icon/image/default) |

## Variants

### Visual Variants

| Variant | Description |
|---------|-------------|
| default | Clean appearance with transparent border |
| outline | Visible border for clear separation |
| muted | Subtle background (bg-muted/50) for grouped items |

### Size Variants

| Size | Padding | Gap | Use Case |
|------|---------|-----|----------|
| default | py-3.5 px-4 | gap-3.5 | Regular content, standard lists |
| sm | py-2.5 px-3 | gap-2.5 | Compact layouts, sidebars |
| xs | py-2 px-2.5 | gap-2 | Dense lists, activity feeds |

### Media Variants

| Variant | Size | Description |
|---------|------|-------------|
| default | auto | Transparent background, flex container |
| icon | auto | Auto-sizes SVG icons to size-4 |
| image | size-10 (default) | Fixed container with rounded corners, scales to size-8 (sm) and size-6 (xs) |

## Examples

### Basic List Item

```tsx
<ItemGroup>
  <Item variant="outline">
    <ItemMedia variant="icon">
      <KeyIcon />
    </ItemMedia>
    <ItemContent>
      <ItemTitle>API Key</ItemTitle>
      <ItemDescription>Production key created 3 months ago</ItemDescription>
    </ItemContent>
    <ItemActions>
      <Button variant="ghost" size="icon-sm">
        <CopyIcon />
      </Button>
    </ItemActions>
  </Item>
</ItemGroup>
```

### With Status Badge

```tsx
<Item variant="outline">
  <ItemMedia variant="icon">
    <WebhookIcon className="text-green-500" />
  </ItemMedia>
  <ItemContent>
    <ItemTitle>
      Production Webhook
      <Badge variant="secondary">Active</Badge>
    </ItemTitle>
    <ItemDescription>https://api.example.com/webhooks/neynar</ItemDescription>
  </ItemContent>
  <ItemActions>
    <Button variant="ghost">Test</Button>
  </ItemActions>
</Item>
```

### Complex Layout with Header/Footer

```tsx
<Item variant="muted">
  <ItemMedia variant="icon">
    <WebhookIcon />
  </ItemMedia>
  <ItemContent>
    <ItemHeader>
      <div className="flex flex-col gap-1">
        <ItemTitle>
          Staging Webhook
          <Badge variant="destructive">Failed</Badge>
        </ItemTitle>
        <ItemDescription>https://staging.example.com/webhooks</ItemDescription>
      </div>
      <ItemActions>
        <Button variant="ghost">Retry</Button>
      </ItemActions>
    </ItemHeader>
    <ItemFooter className="border-border w-full border-t pt-2">
      <div className="flex items-center gap-2 text-sm">
        <XCircleIcon className="text-red-500 size-4" />
        <span className="text-muted-foreground">Last failure: Connection timeout (5xx)</span>
      </div>
    </ItemFooter>
  </ItemContent>
</Item>
```

### Team Member with Avatar

```tsx
<Item>
  <ItemMedia variant="icon">
    <div className="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-full text-sm font-semibold">
      JD
    </div>
  </ItemMedia>
  <ItemContent>
    <ItemTitle>
      Jane Doe
      <Badge variant="outline">Owner</Badge>
    </ItemTitle>
    <ItemDescription>jane.doe@example.com • Full access to all resources</ItemDescription>
  </ItemContent>
  <ItemActions>
    <Button variant="ghost" size="icon-sm">
      <MoreHorizontalIcon />
    </Button>
  </ItemActions>
</Item>
```

### Activity Feed (Compact)

```tsx
<ItemGroup>
  <Item size="xs">
    <ItemMedia variant="icon">
      <CheckCircle2Icon className="text-green-500 size-4" />
    </ItemMedia>
    <ItemContent>
      <ItemTitle>API key created</ItemTitle>
      <ItemDescription>Production key created by jane.doe@example.com</ItemDescription>
    </ItemContent>
    <ItemContent>
      <div className="text-muted-foreground flex items-center gap-1 text-xs">
        <ClockIcon className="size-3" />
        2 hours ago
      </div>
    </ItemContent>
  </Item>
</ItemGroup>
```

### Document List with Images

```tsx
<Item variant="outline">
  <ItemMedia variant="image">
    <div className="bg-gradient-to-br from-purple-500 to-pink-500 flex size-full items-center justify-center">
      <FileIcon className="text-white size-6" />
    </div>
  </ItemMedia>
  <ItemContent>
    <ItemTitle>API Documentation.pdf</ItemTitle>
    <ItemDescription>Last modified 3 days ago • 2.4 MB</ItemDescription>
  </ItemContent>
  <ItemActions>
    <Button variant="ghost">Download</Button>
    <Button variant="ghost" size="icon-sm">
      <ChevronRightIcon />
    </Button>
  </ItemActions>
</Item>
```

### List with Separators

```tsx
<ItemGroup>
  <Item variant="muted">
    <ItemContent>
      <ItemTitle>First Item</ItemTitle>
    </ItemContent>
  </Item>

  <ItemSeparator />

  <Item variant="muted">
    <ItemContent>
      <ItemTitle>Second Item</ItemTitle>
    </ItemContent>
  </Item>
</ItemGroup>
```

## Accessibility

- ItemGroup renders with `role="list"` for semantic list structure
- Focus ring styles on Item when using `render` prop with interactive elements
- ItemDescription automatically truncates with `line-clamp-2` to prevent excessive text
- Links within ItemDescription receive underline and hover styles
- All interactive elements maintain proper focus indicators

## Composition Patterns

### Multiple Content Areas

Use multiple `ItemContent` components for side-by-side content sections:

```tsx
<Item>
  <ItemContent>
    <ItemTitle>Main Content</ItemTitle>
    <ItemDescription>Primary description</ItemDescription>
  </ItemContent>
  <ItemContent>
    <div className="text-muted-foreground text-xs">2 hours ago</div>
  </ItemContent>
</Item>
```

The first `ItemContent` flexes to fill space, subsequent ones use `flex-none`.

### Icon with Background

Wrap icons in colored containers for visual emphasis:

```tsx
<ItemMedia variant="icon">
  <div className="bg-primary/10 text-primary rounded-md p-2">
    <KeyIcon className="size-5" />
  </div>
</ItemMedia>
```

## Related

- [Button](/components/button) - For ItemActions controls
- [Badge](/components/badge) - For status indicators in titles
- [Separator](/components/separator) - Used by ItemSeparator

---

# Kbd

Display keyboard shortcuts and key combinations with consistent styling.

## Import

```tsx
import { Kbd, KbdGroup } from "@neynar/ui/kbd"
```

## Anatomy

```tsx
// Single key
<Kbd>K</Kbd>

// Key combination
<KbdGroup>
  <Kbd><CommandIcon /></Kbd>
  <Kbd>K</Kbd>
</KbdGroup>
```

## Components

| Component | Description |
|-----------|-------------|
| Kbd | Individual keyboard key display (text or icon) |
| KbdGroup | Container for multiple keys pressed together or in sequence |

## Props

### Kbd

Extends standard HTML `<kbd>` element props.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | - | Key text or icon (e.g., "K", `<CommandIcon />`) |
| className | string | - | Additional CSS classes |

**Auto-styling:**
- Fixed height (h-5), auto width with minimum size
- Muted background and foreground colors
- Non-interactive (pointer-events-none, select-none)
- Icons auto-sized to size-3
- Special styling when used in tooltips

### KbdGroup

Extends standard HTML `<div>` element props (rendered as `<kbd>` tag).

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | - | Multiple `<Kbd>` components |
| className | string | - | Additional CSS classes |

**Auto-behaviors:**
- Horizontal flex layout with gap-1
- Use for simultaneous keys (Cmd+K) or sequential keys (G→H)

## Data Attributes

| Attribute | Component | Description |
|-----------|-----------|-------------|
| data-slot="kbd" | Kbd | Identifies individual key element |
| data-slot="kbd-group" | KbdGroup | Identifies key combination container |

## Examples

### Single Keys

```tsx
<div className="flex gap-2">
  <Kbd>A</Kbd>
  <Kbd>Esc</Kbd>
  <Kbd>Enter</Kbd>
  <Kbd>Space</Kbd>
</div>
```

### Keys with Icons

```tsx
import { CommandIcon, ArrowUpIcon, CornerDownLeftIcon } from "lucide-react"

<div className="flex gap-2">
  <Kbd><CommandIcon /></Kbd>
  <Kbd><ArrowUpIcon /></Kbd>
  <Kbd><CornerDownLeftIcon /></Kbd>
</div>
```

Icons are automatically sized to fit the kbd container.

### Key Combinations (Simultaneous)

```tsx
// Cmd+K (command palette)
<KbdGroup>
  <Kbd><CommandIcon /></Kbd>
  <Kbd>K</Kbd>
</KbdGroup>

// Cmd+Shift+P
<KbdGroup>
  <Kbd><CommandIcon /></Kbd>
  <Kbd>Shift</Kbd>
  <Kbd>P</Kbd>
</KbdGroup>

// Ctrl+Alt+Delete
<KbdGroup>
  <Kbd>Ctrl</Kbd>
  <Kbd>Alt</Kbd>
  <Kbd><DeleteIcon /></Kbd>
</KbdGroup>
```

### Sequential Keys (Vim-like)

```tsx
// G then H (Go Home)
<KbdGroup>
  <Kbd>G</Kbd>
  <Kbd>H</Kbd>
</KbdGroup>

// G then D (Go Dashboard)
<KbdGroup>
  <Kbd>G</Kbd>
  <Kbd>D</Kbd>
</KbdGroup>
```

KbdGroup works for both simultaneous and sequential shortcuts - context determines meaning.

### In Documentation/Prose

```tsx
<p>
  To open the command palette, press{" "}
  <KbdGroup>
    <Kbd><CommandIcon /></Kbd>
    <Kbd>K</Kbd>
  </KbdGroup>{" "}
  at any time.
</p>

<p>
  Use <Kbd><ArrowUpIcon /></Kbd> and <Kbd><ArrowDownIcon /></Kbd> to navigate,
  then press <Kbd><CornerDownLeftIcon /></Kbd> to select.
</p>
```

### Keyboard Shortcuts Help Panel

```tsx
function ShortcutsHelp() {
  const shortcuts = [
    {
      keys: <KbdGroup><Kbd><CommandIcon /></Kbd><Kbd>K</Kbd></KbdGroup>,
      description: "Open command palette"
    },
    {
      keys: <KbdGroup><Kbd><CommandIcon /></Kbd><Kbd>S</Kbd></KbdGroup>,
      description: "Save changes"
    },
    {
      keys: <Kbd>Esc</Kbd>,
      description: "Close dialog"
    }
  ]

  return (
    <div className="space-y-2">
      {shortcuts.map((shortcut, idx) => (
        <div key={idx} className="flex justify-between">
          <span>{shortcut.description}</span>
          {shortcut.keys}
        </div>
      ))}
    </div>
  )
}
```

### Common Shortcuts Grid

```tsx
<div className="grid grid-cols-2 gap-4">
  <div className="flex justify-between">
    <span>Save</span>
    <KbdGroup><Kbd><CommandIcon /></Kbd><Kbd>S</Kbd></KbdGroup>
  </div>
  <div className="flex justify-between">
    <span>Copy</span>
    <KbdGroup><Kbd><CommandIcon /></Kbd><Kbd>C</Kbd></KbdGroup>
  </div>
  <div className="flex justify-between">
    <span>Paste</span>
    <KbdGroup><Kbd><CommandIcon /></Kbd><Kbd>V</Kbd></KbdGroup>
  </div>
  <div className="flex justify-between">
    <span>Undo</span>
    <KbdGroup><Kbd><CommandIcon /></Kbd><Kbd>Z</Kbd></KbdGroup>
  </div>
</div>
```

## Accessibility

- Uses semantic `<kbd>` HTML element for keyboard input
- Non-interactive by default (pointer-events-none, select-none)
- Text content is screen-reader accessible
- Icon-only keys may need aria-label on parent for context

## Related

- [Tooltip](/docs/components/tooltip.llm.md) - Often used together to show shortcuts on hover
- [Command](/docs/components/command.llm.md) - Command palette that displays kbd shortcuts

---

# Label

Accessible label component for form controls with automatic disabled state styling.

## Import

```tsx
import { Label } from "@neynar/ui/label"
```

## Anatomy

```tsx
<Label htmlFor="input-id">Label Text</Label>
<Input id="input-id" />
```

## Props

Extends all native HTML `<label>` element props including:

| Prop | Type | Description |
|------|------|-------------|
| htmlFor | string | ID of the associated form control (creates accessible relationship) |
| className | string | Additional CSS classes |
| children | ReactNode | Label content (text, icons, indicators, etc.) |

### Automatic Disabled States

The Label component automatically responds to disabled states:
- **peer-disabled**: When the associated input uses `disabled` prop
- **group-data-[disabled=true]**: When wrapped in a disabled group/fieldset

## Data Attributes

| Attribute | Value |
|-----------|-------|
| data-slot | "label" |

## Examples

### Basic Label with Input

```tsx
<div className="space-y-2">
  <Label htmlFor="email">Email Address</Label>
  <Input id="email" type="email" placeholder="you@example.com" />
</div>
```

### Required Field Indicator

```tsx
<Label htmlFor="username">
  Username
  <span className="text-destructive ml-1">*</span>
</Label>
<Input id="username" required />
```

### Label with Helper Icon

```tsx
import { InfoIcon } from "lucide-react"

<Label htmlFor="api-key">
  API Key
  <InfoIcon className="text-muted-foreground ml-1 size-3.5" />
</Label>
<Input id="api-key" />
<p className="text-muted-foreground text-sm">
  Find your API key in the dashboard settings
</p>
```

### Checkbox Label Pattern

```tsx
<div className="flex items-center space-x-2">
  <Checkbox id="terms" />
  <Label htmlFor="terms" className="font-normal cursor-pointer">
    I agree to the terms and conditions
  </Label>
</div>
```

### Radio Group Label Pattern

```tsx
<div className="space-y-3">
  <Label className="font-medium">Choose an option</Label>
  <RadioGroup defaultValue="option-1">
    <div className="flex items-center space-x-2">
      <RadioGroupItem value="option-1" id="opt-1" />
      <Label htmlFor="opt-1" className="font-normal cursor-pointer">
        Option 1
      </Label>
    </div>
    <div className="flex items-center space-x-2">
      <RadioGroupItem value="option-2" id="opt-2" />
      <Label htmlFor="opt-2" className="font-normal cursor-pointer">
        Option 2
      </Label>
    </div>
  </RadioGroup>
</div>
```

### Switch with Description

```tsx
<div className="flex items-center justify-between">
  <div className="space-y-0.5">
    <Label htmlFor="notifications">Email Notifications</Label>
    <p className="text-muted-foreground text-sm">
      Receive email alerts for updates
    </p>
  </div>
  <Switch id="notifications" />
</div>
```

### Disabled State

```tsx
<div className="space-y-2">
  <Label htmlFor="disabled-input" className="opacity-50">
    Disabled Field
  </Label>
  <Input id="disabled-input" disabled />
</div>
```

### Error State

```tsx
<div className="space-y-2">
  <Label htmlFor="error-input" className="text-destructive">
    Invalid Field
  </Label>
  <Input id="error-input" aria-invalid="true" />
  <p className="text-destructive text-sm">This field is required</p>
</div>
```

### Horizontal Form Layout

```tsx
<div className="grid grid-cols-3 items-center gap-4">
  <Label htmlFor="username" className="text-right">
    Username
  </Label>
  <Input id="username" className="col-span-2" />
</div>
```

### Complex Label with Badge

```tsx
<Label htmlFor="api-endpoint" className="flex items-center gap-2">
  API Endpoint
  <span className="bg-primary/10 text-primary rounded px-1.5 py-0.5 text-xs font-medium">
    Required
  </span>
</Label>
<Input id="api-endpoint" required />
```

### Label with Link

```tsx
<Label htmlFor="webhook">
  Webhook URL
  <a href="/docs/webhooks" className="text-primary hover:underline ml-1 text-xs font-normal">
    (Learn more)
  </a>
</Label>
<Input id="webhook" type="url" />
```

## Accessibility

- Uses native `<label>` element for proper form control association
- Clicking the label focuses/activates the associated control via `htmlFor` prop
- Automatically handles disabled state styling via `peer-disabled` and `group-data-[disabled]`
- Screen readers announce the label text when the associated control receives focus
- Supports nested content (icons, badges, links) while maintaining accessibility

## Styling Patterns

### Font Weight Variations

```tsx
{/* Default: font-medium */}
<Label htmlFor="default">Default Weight</Label>

{/* Normal weight for checkbox/radio labels */}
<Label htmlFor="checkbox" className="font-normal">
  Checkbox Option
</Label>
```

### Cursor Styles

```tsx
{/* Clickable for checkbox/radio */}
<Label htmlFor="check" className="cursor-pointer">

{/* Not allowed for disabled */}
<Label htmlFor="disabled" className="cursor-not-allowed">
```

## Related

- [Input](./input.llm.md) - Text input component
- [Checkbox](./checkbox.llm.md) - Checkbox component
- [Radio Group](./radio-group.llm.md) - Radio button group
- [Switch](./switch.llm.md) - Toggle switch
- [Textarea](./textarea.llm.md) - Multi-line text input

---

# Menubar

Horizontal menubar for application-level navigation and actions, commonly seen in desktop applications with File, Edit, View, and Help menus.

## Import

```tsx
import {
  Menubar,
  MenubarMenu,
  MenubarTrigger,
  MenubarContent,
  MenubarItem,
  MenubarSeparator,
  MenubarShortcut,
  MenubarCheckboxItem,
  MenubarRadioGroup,
  MenubarRadioItem,
  MenubarSub,
  MenubarSubTrigger,
  MenubarSubContent,
  MenubarLabel,
  MenubarGroup,
} from "@neynar/ui/menubar"
```

## Anatomy

```tsx
<Menubar>
  <MenubarMenu>
    <MenubarTrigger>File</MenubarTrigger>
    <MenubarContent>
      <MenubarItem>New</MenubarItem>
      <MenubarItem>Open</MenubarItem>
      <MenubarSeparator />
      <MenubarSub>
        <MenubarSubTrigger>Export</MenubarSubTrigger>
        <MenubarSubContent>
          <MenubarItem>PDF</MenubarItem>
          <MenubarItem>CSV</MenubarItem>
        </MenubarSubContent>
      </MenubarSub>
    </MenubarContent>
  </MenubarMenu>
  <MenubarMenu>
    <MenubarTrigger>Edit</MenubarTrigger>
    <MenubarContent>
      <MenubarItem>Undo</MenubarItem>
      <MenubarItem>Redo</MenubarItem>
    </MenubarContent>
  </MenubarMenu>
</Menubar>
```

## Components

| Component | Description |
|-----------|-------------|
| Menubar | Root container for the horizontal menubar |
| MenubarMenu | Individual menu within the menubar (manages open/close state) |
| MenubarTrigger | Button that opens the menu dropdown |
| MenubarContent | Dropdown panel containing menu items (auto-portals) |
| MenubarItem | Interactive menu item with optional variants |
| MenubarCheckboxItem | Menu item with checkbox for toggles |
| MenubarRadioGroup | Container for mutually exclusive radio items |
| MenubarRadioItem | Radio button menu item (use inside MenubarRadioGroup) |
| MenubarSub | Container for nested submenu |
| MenubarSubTrigger | Menu item that opens a submenu |
| MenubarSubContent | Dropdown panel for submenu items |
| MenubarGroup | Logical grouping of menu items |
| MenubarLabel | Non-interactive section label |
| MenubarSeparator | Visual divider between items |
| MenubarShortcut | Keyboard shortcut hint (display only) |

## Props

### Menubar

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| loopFocus | boolean | true | Loop keyboard focus back to first item at end of list |
| modal | boolean | true | Whether menubar is modal |
| disabled | boolean | false | Disable entire menubar |
| orientation | "horizontal" \| "vertical" | "horizontal" | Menubar orientation |
| className | string | - | Additional CSS classes |

### MenubarMenu

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | - | Controlled open state |
| defaultOpen | boolean | - | Default open state (uncontrolled) |
| onOpenChange | (open: boolean) => void | - | Callback when open state changes |

### MenubarContent

Automatically renders in a portal with backdrop blur (frosted glass effect).

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| align | "start" \| "center" \| "end" | "start" | Alignment relative to trigger |
| alignOffset | number | -4 | Offset along alignment axis |
| side | "top" \| "right" \| "bottom" \| "left" | "bottom" | Preferred side of trigger |
| sideOffset | number | 8 | Distance from trigger |

### MenubarItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| disabled | boolean | false | Disable item |
| onClick | MouseEventHandler | - | Click handler |
| closeOnClick | boolean | true | Close menu on click |
| variant | "default" \| "destructive" \| "success" \| "warning" \| "info" | "default" | Visual variant |
| inset | boolean | false | Add left padding for alignment with icons/checkboxes |

### MenubarCheckboxItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| checked | boolean | - | Controlled checked state |
| defaultChecked | boolean | - | Default checked state (uncontrolled) |
| onCheckedChange | (checked: boolean) => void | - | Callback when checked state changes |
| disabled | boolean | false | Disable item |

### MenubarRadioGroup

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | any | - | Controlled selected value |
| defaultValue | any | - | Default selected value (uncontrolled) |
| onValueChange | (value: any) => void | - | Callback when value changes |

### MenubarRadioItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | any | **required** | Value when this item is selected |
| disabled | boolean | false | Disable item |

### MenubarSubTrigger

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| disabled | boolean | false | Disable submenu trigger |
| inset | boolean | false | Add left padding for alignment |

## Data Attributes

### MenubarTrigger
- `aria-expanded` - Present when menu is open

### MenubarItem
- `data-highlighted` - Present when item is highlighted
- `data-disabled` - Present when item is disabled

### MenubarCheckboxItem
- `data-checked` - Present when checked
- `data-disabled` - Present when disabled

### MenubarRadioItem
- `data-checked` - Present when selected
- `data-disabled` - Present when disabled

## Variants

MenubarItem supports semantic variants:

| Variant | Use Case |
|---------|----------|
| default | Standard menu actions |
| destructive | Dangerous actions (delete, sign out) |
| success | Positive actions (approve, confirm) |
| warning | Caution actions (mark for review) |
| info | Informational actions (view details) |

## Examples

### Basic Menubar

```tsx
<Menubar>
  <MenubarMenu>
    <MenubarTrigger>File</MenubarTrigger>
    <MenubarContent>
      <MenubarItem>New</MenubarItem>
      <MenubarItem>Open</MenubarItem>
      <MenubarItem>Save</MenubarItem>
    </MenubarContent>
  </MenubarMenu>

  <MenubarMenu>
    <MenubarTrigger>Edit</MenubarTrigger>
    <MenubarContent>
      <MenubarItem>Undo</MenubarItem>
      <MenubarItem>Redo</MenubarItem>
    </MenubarContent>
  </MenubarMenu>
</Menubar>
```

### With Icons and Shortcuts

```tsx
<Menubar>
  <MenubarMenu>
    <MenubarTrigger>File</MenubarTrigger>
    <MenubarContent>
      <MenubarItem>
        <FileIcon />
        New File
        <MenubarShortcut>⌘N</MenubarShortcut>
      </MenubarItem>
      <MenubarItem>
        <FolderOpenIcon />
        Open
        <MenubarShortcut>⌘O</MenubarShortcut>
      </MenubarItem>
      <MenubarSeparator />
      <MenubarItem>
        <SaveIcon />
        Save
        <MenubarShortcut>⌘S</MenubarShortcut>
      </MenubarItem>
    </MenubarContent>
  </MenubarMenu>
</Menubar>
```

### Checkbox Items (View Settings)

```tsx
function ViewMenu() {
  const [showSidebar, setShowSidebar] = useState(true)
  const [showToolbar, setShowToolbar] = useState(true)

  return (
    <MenubarMenu>
      <MenubarTrigger>View</MenubarTrigger>
      <MenubarContent>
        <MenubarGroup>
          <MenubarLabel>Panels</MenubarLabel>
          <MenubarCheckboxItem
            checked={showSidebar}
            onCheckedChange={setShowSidebar}
          >
            Sidebar
          </MenubarCheckboxItem>
          <MenubarCheckboxItem
            checked={showToolbar}
            onCheckedChange={setShowToolbar}
          >
            Toolbar
          </MenubarCheckboxItem>
        </MenubarGroup>
      </MenubarContent>
    </MenubarMenu>
  )
}
```

### Radio Group (Theme Selection)

```tsx
function ThemeMenu() {
  const [theme, setTheme] = useState("system")

  return (
    <MenubarSub>
      <MenubarSubTrigger>
        <SunIcon />
        Theme
      </MenubarSubTrigger>
      <MenubarSubContent>
        <MenubarRadioGroup value={theme} onValueChange={setTheme}>
          <MenubarRadioItem value="light">
            <SunIcon />
            Light
          </MenubarRadioItem>
          <MenubarRadioItem value="dark">
            <MoonIcon />
            Dark
          </MenubarRadioItem>
          <MenubarRadioItem value="system">
            <MonitorIcon />
            System
          </MenubarRadioItem>
        </MenubarRadioGroup>
      </MenubarSubContent>
    </MenubarSub>
  )
}
```

### Nested Submenus

```tsx
<MenubarMenu>
  <MenubarTrigger>File</MenubarTrigger>
  <MenubarContent>
    <MenubarItem>New File</MenubarItem>
    <MenubarSub>
      <MenubarSubTrigger>
        <DownloadIcon />
        Export
      </MenubarSubTrigger>
      <MenubarSubContent>
        <MenubarItem>Export as JSON</MenubarItem>
        <MenubarItem>Export as CSV</MenubarItem>
        <MenubarSeparator />
        <MenubarSub>
          <MenubarSubTrigger>More Formats</MenubarSubTrigger>
          <MenubarSubContent>
            <MenubarItem>YAML</MenubarItem>
            <MenubarItem>TOML</MenubarItem>
          </MenubarSubContent>
        </MenubarSub>
      </MenubarSubContent>
    </MenubarSub>
  </MenubarContent>
</MenubarMenu>
```

### Item Variants

```tsx
<MenubarMenu>
  <MenubarTrigger>Actions</MenubarTrigger>
  <MenubarContent>
    <MenubarItem variant="success">
      <CheckCircleIcon />
      Approve
    </MenubarItem>
    <MenubarItem variant="warning">
      <AlertTriangleIcon />
      Mark for Review
    </MenubarItem>
    <MenubarItem variant="info">
      <InfoIcon />
      View Details
    </MenubarItem>
    <MenubarSeparator />
    <MenubarItem variant="destructive">
      <LogOutIcon />
      Sign Out
    </MenubarItem>
  </MenubarContent>
</MenubarMenu>
```

## Keyboard Navigation

| Key | Action |
|-----|--------|
| Tab | Move focus to next menubar trigger |
| Shift+Tab | Move focus to previous menubar trigger |
| Enter/Space | Open menu or activate item |
| Arrow Down | Open menu or move to next item |
| Arrow Up | Move to previous item |
| Arrow Right | Move to next menubar trigger (or open submenu) |
| Arrow Left | Move to previous menubar trigger (or close submenu) |
| Escape | Close open menu |
| Home | Focus first item |
| End | Focus last item |

## Accessibility

- Full keyboard navigation with arrow keys and Tab
- ARIA attributes automatically managed (aria-expanded, aria-haspopup, role="menubar")
- Focus management with automatic focus trapping in open menus
- Screen reader announcements for menu state changes
- Supports `disabled` state with proper ARIA attributes

## Related

- [DropdownMenu](./dropdown-menu.llm.md) - For single-menu dropdowns
- [ContextMenu](./context-menu.llm.md) - For right-click menus
- [NavigationMenu](./navigation-menu.llm.md) - For site navigation with links

---

# NavigationMenu

Horizontal navigation menu with mega menu support for main site navigation and dashboard headers.

## Import

```tsx
import {
  NavigationMenu,
  NavigationMenuList,
  NavigationMenuItem,
  NavigationMenuTrigger,
  NavigationMenuContent,
  NavigationMenuLink,
} from "@neynar/ui/navigation-menu"
```

## Anatomy

```tsx
<NavigationMenu>
  <NavigationMenuList>
    <NavigationMenuItem value="item-1">
      <NavigationMenuTrigger>Products</NavigationMenuTrigger>
      <NavigationMenuContent>
        {/* Mega menu content */}
      </NavigationMenuContent>
    </NavigationMenuItem>

    <NavigationMenuItem value="item-2">
      <NavigationMenuLink href="/pricing">Pricing</NavigationMenuLink>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>
```

## Components

| Component | Description |
|-----------|-------------|
| NavigationMenu | Root container, manages active state and keyboard navigation |
| NavigationMenuList | Container for navigation items, renders as horizontal list |
| NavigationMenuItem | Individual item, can contain Link or Trigger + Content |
| NavigationMenuTrigger | Button that opens content panel, includes chevron icon |
| NavigationMenuContent | Mega menu content panel with smooth transitions |
| NavigationMenuLink | Clickable link for direct navigation or panel content |
| NavigationMenuIndicator | Optional visual indicator (rarely used) |

## Props

### NavigationMenu

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | - | Controlled active item value |
| defaultValue | string | - | Default active item (uncontrolled) |
| onValueChange | (value: string) => void | - | Called when active item changes |
| orientation | "horizontal" \| "vertical" | "horizontal" | Menu orientation |
| className | string | - | Additional CSS classes |

### NavigationMenuItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | - | Unique identifier for controlled state |
| className | string | - | Additional CSS classes |

### NavigationMenuTrigger

Automatically includes a rotating chevron icon that animates on open/close.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| render | ReactElement | - | Custom render element |

### NavigationMenuContent

Automatically renders in a positioned portal with backdrop and smooth transitions.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |

### NavigationMenuLink

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| href | string | - | Link destination |
| active | boolean | false | Highlights link as active/current page |
| closeOnClick | boolean | true | Whether to close menu on click |
| className | string | - | Additional CSS classes |

### NavigationMenuList

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |

## Data Attributes

### NavigationMenuTrigger

| Attribute | When Present |
|-----------|--------------|
| data-open | Content panel is open |
| data-popup-open | Content panel is open |
| data-pressed | Trigger is pressed |

### NavigationMenuContent

| Attribute | When Present |
|-----------|--------------|
| data-open | Content is visible |
| data-closed | Content is hidden |
| data-starting-style | During opening animation |
| data-ending-style | During closing animation |

### NavigationMenuLink

| Attribute | When Present |
|-----------|--------------|
| data-active | Link is active/current page |

## Examples

### Basic Link Navigation

```tsx
<NavigationMenu>
  <NavigationMenuList>
    <NavigationMenuItem value="home">
      <NavigationMenuLink href="/">Home</NavigationMenuLink>
    </NavigationMenuItem>
    <NavigationMenuItem value="about">
      <NavigationMenuLink href="/about">About</NavigationMenuLink>
    </NavigationMenuItem>
    <NavigationMenuItem value="contact">
      <NavigationMenuLink href="/contact">Contact</NavigationMenuLink>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>
```

### Mega Menu with Product Showcase

```tsx
<NavigationMenu>
  <NavigationMenuList>
    <NavigationMenuItem value="products">
      <NavigationMenuTrigger>Products</NavigationMenuTrigger>
      <NavigationMenuContent>
        <div className="w-[600px] p-4">
          <div className="mb-4">
            <h4 className="font-semibold">Our Products</h4>
            <p className="text-muted-foreground text-sm">
              Everything you need to build on Farcaster
            </p>
          </div>
          <div className="grid grid-cols-2 gap-4">
            <NavigationMenuLink href="/api" className="block rounded-md">
              <div className="flex items-start gap-3">
                <div className="bg-primary/10 text-primary rounded-md p-2">
                  <CodeIcon className="size-5" />
                </div>
                <div>
                  <div className="font-medium">Farcaster API</div>
                  <div className="text-muted-foreground text-xs">
                    REST APIs for casts, users, and feeds
                  </div>
                </div>
              </div>
            </NavigationMenuLink>
            {/* More product cards */}
          </div>
        </div>
      </NavigationMenuContent>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>
```

### Grouped Content Panels

```tsx
<NavigationMenu>
  <NavigationMenuList>
    <NavigationMenuItem value="resources">
      <NavigationMenuTrigger>Resources</NavigationMenuTrigger>
      <NavigationMenuContent>
        <div className="grid w-[400px] grid-cols-2 gap-4 p-4">
          <div className="space-y-3">
            <h4 className="text-muted-foreground text-xs font-semibold uppercase">
              Documentation
            </h4>
            <div className="space-y-2">
              <NavigationMenuLink href="/docs" className="block">
                Getting Started
              </NavigationMenuLink>
              <NavigationMenuLink href="/api-reference" className="block">
                API Reference
              </NavigationMenuLink>
            </div>
          </div>
          <div className="space-y-3">
            <h4 className="text-muted-foreground text-xs font-semibold uppercase">
              Community
            </h4>
            <div className="space-y-2">
              <NavigationMenuLink href="/discord" className="block">
                Discord
              </NavigationMenuLink>
              <NavigationMenuLink href="/github" className="block">
                GitHub
              </NavigationMenuLink>
            </div>
          </div>
        </div>
      </NavigationMenuContent>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>
```

### With Icons

```tsx
<NavigationMenu>
  <NavigationMenuList>
    <NavigationMenuItem value="home">
      <NavigationMenuLink href="/" className="flex items-center gap-2">
        <HomeIcon className="size-4" />
        Home
      </NavigationMenuLink>
    </NavigationMenuItem>
    <NavigationMenuItem value="apps">
      <NavigationMenuTrigger>
        <LayoutDashboardIcon className="mr-2 size-4" />
        Apps
      </NavigationMenuTrigger>
      <NavigationMenuContent>
        <div className="w-[280px] space-y-2 p-4">
          <NavigationMenuLink href="/my-apps" className="flex items-center gap-2">
            <RocketIcon className="size-4" />
            My Apps
          </NavigationMenuLink>
          <NavigationMenuLink href="/templates" className="flex items-center gap-2">
            <PackageIcon className="size-4" />
            Templates
          </NavigationMenuLink>
        </div>
      </NavigationMenuContent>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>
```

### Active State

```tsx
function Nav({ currentPath }: { currentPath: string }) {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem value="dashboard">
          <NavigationMenuLink
            href="/dashboard"
            active={currentPath === "/dashboard"}
          >
            Dashboard
          </NavigationMenuLink>
        </NavigationMenuItem>
        <NavigationMenuItem value="analytics">
          <NavigationMenuLink
            href="/analytics"
            active={currentPath === "/analytics"}
          >
            Analytics
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}
```

## Keyboard Navigation

| Key | Action |
|-----|--------|
| Tab | Move focus to next item |
| Shift + Tab | Move focus to previous item |
| Arrow Right | Focus next item (horizontal) |
| Arrow Left | Focus previous item (horizontal) |
| Enter / Space | Activate trigger or link |
| Escape | Close open content panel |
| Home | Focus first item |
| End | Focus last item |

## Accessibility

- Implements ARIA navigation menu pattern with proper roles and states
- Keyboard navigation follows WAI-ARIA authoring practices
- Focus management automatically moves between items and panels
- Screen readers announce active states and panel open/close

## Styling Notes

- Content panel width is controlled by content wrapper (e.g., `w-[600px]`)
- Use grid layouts for multi-column mega menus
- Icons automatically size to `size-4` when not explicitly sized
- Active links receive muted background by default
- Smooth transitions use cubic-bezier easing for polish

## Related

- [Tabs](/components/tabs) - For tabbed content switching
- [DropdownMenu](/components/dropdown-menu) - For action menus
- [Command](/components/command) - For command palettes
- [Breadcrumb](/components/breadcrumb) - For hierarchical navigation

---

# Pagination

A composable pagination component for navigating multi-page data sets with semantic HTML and full keyboard accessibility.

## Import

```tsx
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationPrevious,
  PaginationNext,
  PaginationEllipsis,
} from "@neynar/ui/pagination"
```

## Anatomy

```tsx
<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="#" />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" isActive>2</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">3</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">10</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="#" />
    </PaginationItem>
  </PaginationContent>
</Pagination>
```

## Components

| Component | Description |
|-----------|-------------|
| `Pagination` | Root navigation container with semantic `<nav>` element |
| `PaginationContent` | Flex list container for pagination items |
| `PaginationItem` | Wrapper for individual pagination elements |
| `PaginationLink` | Clickable page number link |
| `PaginationPrevious` | Previous page control with chevron + "Previous" label |
| `PaginationNext` | Next page control with chevron + "Next" label |
| `PaginationEllipsis` | Ellipsis indicator for skipped pages |

## Props

### Pagination

Extends `React.ComponentProps<"nav">`.

Root container with `role="navigation"` and `aria-label="pagination"`.

### PaginationContent

Extends `React.ComponentProps<"ul">`.

Renders as a flexbox list with gap spacing between items.

### PaginationItem

Extends `React.ComponentProps<"li">`.

Simple list item wrapper for pagination elements.

### PaginationLink

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `isActive` | `boolean` | `false` | Highlights the current active page |
| `size` | `"default" \| "sm" \| "lg" \| "icon"` | `"icon"` | Button size variant |
| `href` | `string` | - | Link destination |

Plus all standard `<a>` element props.

**Notes:**
- Uses Button component with `render` prop for link semantics
- Active pages use `outline` variant, inactive use `ghost`
- Sets `aria-current="page"` when `isActive={true}`
- Renders with `data-active` attribute for custom styling

### PaginationPrevious

Extends `PaginationLink` props.

**Auto-behaviors:**
- Sets `aria-label="Go to previous page"`
- Uses `size="default"` by default
- Displays chevron icon + "Previous" text (text hidden on mobile via `hidden sm:block`)

### PaginationNext

Extends `PaginationLink` props.

**Auto-behaviors:**
- Sets `aria-label="Go to next page"`
- Uses `size="default"` by default
- Displays "Next" text (hidden on mobile) + chevron icon

### PaginationEllipsis

Extends `React.ComponentProps<"span">`.

**Auto-behaviors:**
- Sets `aria-hidden` to hide from screen readers
- Includes screen reader text "More pages"
- Displays horizontal ellipsis icon

## Data Attributes

All components include `data-slot` attributes for targeted styling:

| Attribute | Component |
|-----------|-----------|
| `data-slot="pagination"` | Pagination |
| `data-slot="pagination-content"` | PaginationContent |
| `data-slot="pagination-item"` | PaginationItem |
| `data-slot="pagination-link"` | PaginationLink |
| `data-slot="pagination-ellipsis"` | PaginationEllipsis |

PaginationLink also includes:
- `data-active="true"` when `isActive={true}`

## Examples

### Basic Pagination

```tsx
<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="#" />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" isActive>2</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">3</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="#" />
    </PaginationItem>
  </PaginationContent>
</Pagination>
```

### With Ellipsis (Large Data Sets)

```tsx
<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="#" />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" isActive>2</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">3</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">42</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="#" />
    </PaginationItem>
  </PaginationContent>
</Pagination>
```

### Controlled with React State

```tsx
function APILogsTable() {
  const [currentPage, setCurrentPage] = useState(1)
  const totalPages = 42

  function handlePageClick(page: number) {
    setCurrentPage(page)
  }

  return (
    <div>
      {/* Your table content */}

      <Pagination>
        <PaginationContent>
          <PaginationItem>
            <PaginationPrevious
              href="#"
              onClick={(e) => {
                e.preventDefault()
                if (currentPage > 1) handlePageClick(currentPage - 1)
              }}
            />
          </PaginationItem>
          <PaginationItem>
            <PaginationLink
              href="#"
              isActive={currentPage === 1}
              onClick={(e) => {
                e.preventDefault()
                handlePageClick(1)
              }}
            >
              1
            </PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationLink
              href="#"
              isActive={currentPage === 2}
              onClick={(e) => {
                e.preventDefault()
                handlePageClick(2)
              }}
            >
              2
            </PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationEllipsis />
          </PaginationItem>
          <PaginationItem>
            <PaginationLink
              href="#"
              onClick={(e) => {
                e.preventDefault()
                handlePageClick(totalPages)
              }}
            >
              {totalPages}
            </PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationNext
              href="#"
              onClick={(e) => {
                e.preventDefault()
                if (currentPage < totalPages) handlePageClick(currentPage + 1)
              }}
            />
          </PaginationItem>
        </PaginationContent>
      </Pagination>
    </div>
  )
}
```

### Middle Page State (Dual Ellipsis)

```tsx
<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="#" />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">12</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" isActive>13</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">14</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">25</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="#" />
    </PaginationItem>
  </PaginationContent>
</Pagination>
```

### Minimal (Controls Only)

```tsx
<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="#" />
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="#" />
    </PaginationItem>
  </PaginationContent>
</Pagination>
```

## Accessibility

- Uses semantic `<nav>` element with `role="navigation"` and `aria-label="pagination"`
- Active page links include `aria-current="page"` for screen reader context
- Previous/Next controls have descriptive `aria-label` attributes
- Ellipsis is hidden from screen readers via `aria-hidden` but includes visually hidden text "More pages"
- All links are keyboard navigable via Tab key
- Mobile-responsive: "Previous"/"Next" labels hidden on small screens

## Related

- [Button](./button.llm.md) - Used internally for pagination links

---

# Popover

Floating content panel anchored to a trigger element for contextual information, forms, or interactive content.

## Import

```tsx
import {
  Popover,
  PopoverTrigger,
  PopoverContent,
  PopoverHeader,
  PopoverTitle,
  PopoverDescription,
} from "@neynar/ui/popover"
```

## Anatomy

```tsx
<Popover>
  <PopoverTrigger>
    <Button>Open</Button>
  </PopoverTrigger>
  <PopoverContent>
    <PopoverHeader>
      <PopoverTitle>Title</PopoverTitle>
      <PopoverDescription>Description</PopoverDescription>
    </PopoverHeader>
    {/* Content */}
  </PopoverContent>
</Popover>
```

## Components

| Component | Description |
|-----------|-------------|
| Popover | Root container managing open state |
| PopoverTrigger | Button that opens the popover |
| PopoverContent | Main content panel with portal and positioning |
| PopoverHeader | Optional wrapper for title and description |
| PopoverTitle | Heading label (h2 element) |
| PopoverDescription | Optional description text |

## Props

### Popover

Root component managing state. Use `open`/`onOpenChange` for controlled state.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | - | Controlled open state |
| defaultOpen | boolean | false | Initial open state (uncontrolled) |
| onOpenChange | (open: boolean) => void | - | Callback when open state changes |
| modal | boolean \| "trap-focus" | false | Modal behavior: `true` locks scroll and traps focus, `"trap-focus"` only traps focus |

### PopoverTrigger

Opens the popover when clicked. Renders a button by default.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| render | ReactElement | - | Custom element to use as trigger |
| openOnHover | boolean | false | Open popover on hover instead of click |
| delay | number | 300 | Hover delay in ms (requires openOnHover) |

### PopoverContent

Automatically renders portal and positioner. Includes fade and zoom animations.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| side | "top" \| "right" \| "bottom" \| "left" | "bottom" | Which side of trigger to display on |
| align | "start" \| "center" \| "end" | "center" | Alignment relative to trigger |
| sideOffset | number | 4 | Distance from trigger in pixels |
| alignOffset | number | 0 | Offset along alignment axis in pixels |
| className | string | - | Additional CSS classes (default width is w-72) |

### PopoverHeader

Optional container for title and description with consistent spacing.

Standard `div` props.

### PopoverTitle

Heading label with ARIA attributes. Renders h2 element.

Standard Base UI Title props (className, render, etc).

### PopoverDescription

Optional description text styled with muted foreground.

Standard Base UI Description props (className, render, etc).

## Data Attributes

Available on PopoverContent for styling:

| Attribute | When Present |
|-----------|--------------|
| data-open | Popover is open |
| data-closed | Popover is closed |
| data-side | Value is current side (top/right/bottom/left) |
| data-starting-style | During opening animation |
| data-ending-style | During closing animation |

## Examples

### User Profile Preview

```tsx
<Popover>
  <PopoverTrigger>
    <button className="flex items-center gap-2">
      <Avatar />
      <span>vitalik.eth</span>
    </button>
  </PopoverTrigger>
  <PopoverContent className="w-80" side="bottom" align="start">
    <div className="space-y-3">
      <div className="flex items-start gap-3">
        <Avatar size="lg" />
        <div className="flex-1">
          <p className="font-semibold">vitalik.eth</p>
          <p className="text-muted-foreground text-sm">@vitalik</p>
        </div>
        <Button size="sm">Follow</Button>
      </div>
      <p className="text-sm">Creator of Ethereum...</p>
    </div>
  </PopoverContent>
</Popover>
```

### Filter Form

```tsx
function FilterPopover() {
  const [open, setOpen] = useState(false)
  const [dateRange, setDateRange] = useState("last-7-days")

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger>
        <Button variant="outline" size="sm">
          <SettingsIcon className="mr-2 size-4" />
          Filters
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-80" side="bottom" align="end">
        <PopoverHeader>
          <PopoverTitle>Filter Options</PopoverTitle>
          <PopoverDescription>
            Customize the data displayed
          </PopoverDescription>
        </PopoverHeader>
        <div className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="date-range">Date Range</Label>
            <select
              id="date-range"
              value={dateRange}
              onChange={(e) => setDateRange(e.target.value)}
            >
              <option value="last-7-days">Last 7 days</option>
              <option value="last-30-days">Last 30 days</option>
            </select>
          </div>
          <div className="flex justify-end gap-2">
            <Button variant="ghost" size="sm" onClick={() => setOpen(false)}>
              Cancel
            </Button>
            <Button size="sm" onClick={() => setOpen(false)}>
              Apply
            </Button>
          </div>
        </div>
      </PopoverContent>
    </Popover>
  )
}
```

### Contextual Help

```tsx
<div className="flex items-center gap-2">
  <Label htmlFor="api-key">API Key</Label>
  <Popover>
    <PopoverTrigger>
      <button className="text-muted-foreground hover:text-foreground">
        <HelpCircleIcon className="size-4" />
      </button>
    </PopoverTrigger>
    <PopoverContent className="w-80" side="right">
      <PopoverHeader>
        <PopoverTitle>About API Keys</PopoverTitle>
      </PopoverHeader>
      <div className="space-y-2 text-sm">
        <p>API keys authenticate requests to the Neynar API...</p>
        <ul className="text-muted-foreground ml-4 list-disc space-y-1">
          <li>Production keys have higher rate limits</li>
          <li>Development keys are for testing only</li>
        </ul>
      </div>
    </PopoverContent>
  </Popover>
</div>
```

### Custom Trigger Element

```tsx
<Popover>
  <PopoverTrigger>
    <Button variant="ghost" size="icon-sm">
      <InfoIcon className="size-4" />
    </Button>
  </PopoverTrigger>
  <PopoverContent className="w-80" side="left">
    <PopoverHeader>
      <PopoverTitle>Billing Information</PopoverTitle>
      <PopoverDescription>
        Your subscription details
      </PopoverDescription>
    </PopoverHeader>
    <div className="space-y-3 text-sm">
      <div className="flex justify-between">
        <span className="text-muted-foreground">Current plan</span>
        <span className="font-medium">Pro ($99/mo)</span>
      </div>
      <div className="flex justify-between">
        <span className="text-muted-foreground">Next charge</span>
        <span className="font-medium">$99.00</span>
      </div>
    </div>
  </PopoverContent>
</Popover>
```

### Hover Trigger

```tsx
<Popover>
  <PopoverTrigger openOnHover delay={200}>
    <span className="underline decoration-dotted">Hover me</span>
  </PopoverTrigger>
  <PopoverContent side="top">
    <p className="text-sm">This opens on hover with 200ms delay</p>
  </PopoverContent>
</Popover>
```

## Keyboard

| Key | Action |
|-----|--------|
| Escape | Close popover |
| Tab | Navigate focus within popover |

## Accessibility

- PopoverTitle automatically provides `aria-labelledby` for the popover
- PopoverDescription provides `aria-describedby` when present
- Focus is trapped within popover when `modal` is true or "trap-focus"
- Popover closes on Escape key press
- Focus returns to trigger when popover closes

## Related

- **Tooltip** - For simple text hints without interaction
- **Dropdown Menu** - For menus with selectable actions
- **Dialog** - For modal content requiring user action
- **Hover Card** - Similar to Popover but specifically for hover interactions

---

# Progress

Progress bar for displaying task completion, loading states, and quota usage.

## Import

```tsx
import {
  Progress,
  ProgressTrack,
  ProgressIndicator,
  ProgressLabel,
  ProgressValue,
} from "@neynar/ui/progress"
```

## Anatomy

```tsx
<Progress value={65}>
  <ProgressLabel>Loading</ProgressLabel>
  <ProgressValue />
  <ProgressTrack>
    <ProgressIndicator />
  </ProgressTrack>
</Progress>
```

## Components

| Component | Description |
|-----------|-------------|
| Progress | Root container managing progress state and rendering |
| ProgressLabel | Text label describing the task |
| ProgressValue | Formatted percentage display (auto-positioned right) |
| ProgressTrack | Container for the progress bar track |
| ProgressIndicator | Visual bar showing completion (auto-width based on value) |

## Props

### Progress

Root component providing progress state context to all children.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | number \| null | - | Progress value 0-100, or null for indeterminate |
| min | number | 0 | Minimum value |
| max | number | 100 | Maximum value |
| aria-valuetext | string | - | Custom accessible label for current value |
| getAriaValueText | (formatted: string \| null, value: number \| null) => string | - | Function to generate accessible label |
| locale | Intl.LocalesArgument | - | Locale for number formatting |
| format | Intl.NumberFormatOptions | - | Options for value formatting |
| className | string | - | Additional CSS classes |

### ProgressTrack

Container for the progress indicator. Customize height via className.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Override default styles (default: `h-1.5`) |

**Default styles**: `bg-muted h-1.5 rounded-full relative flex w-full items-center overflow-x-hidden`

### ProgressIndicator

Visual bar showing task completion. Width automatically adjusts based on parent Progress value.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Override colors/animation (default: `bg-primary`) |

**Default styles**: `bg-primary h-full transition-all`

Color customization examples:
- Success: `bg-green-500 dark:bg-green-400`
- Warning: `bg-yellow-500 dark:bg-yellow-400`
- Danger: `bg-red-500 dark:bg-red-400`

### ProgressLabel

Text label for the progress task.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | ReactNode | - | Label text |

**Default styles**: `text-sm font-medium`

### ProgressValue

Displays formatted progress value (e.g., "65%").

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |
| children | (formatted: string \| null, value: number \| null) => ReactNode | - | Custom formatter function |

**Default styles**: `text-muted-foreground ml-auto text-sm tabular-nums`

## Data Attributes

All components share these data attributes for styling:

| Attribute | When Present |
|-----------|--------------|
| data-progressing | Value is between min and max |
| data-complete | Value equals max (100 by default) |
| data-indeterminate | Value is null (unknown duration) |

## Examples

### Basic Progress

```tsx
<Progress value={65}>
  <ProgressLabel>Loading</ProgressLabel>
  <ProgressValue />
  <ProgressTrack>
    <ProgressIndicator />
  </ProgressTrack>
</Progress>
```

### Minimal (No Label/Value)

```tsx
<Progress value={45}>
  <ProgressTrack>
    <ProgressIndicator />
  </ProgressTrack>
</Progress>
```

### Semantic Colors for States

```tsx
// Success state (healthy usage)
<Progress value={45}>
  <ProgressLabel>API Usage</ProgressLabel>
  <ProgressValue />
  <ProgressTrack>
    <ProgressIndicator className="bg-green-500 dark:bg-green-400" />
  </ProgressTrack>
</Progress>

// Warning state (approaching limit)
<Progress value={78}>
  <ProgressLabel>Rate Limit</ProgressLabel>
  <ProgressValue />
  <ProgressTrack>
    <ProgressIndicator className="bg-yellow-500 dark:bg-yellow-400" />
  </ProgressTrack>
</Progress>

// Critical state
<Progress value={94}>
  <ProgressLabel>Storage</ProgressLabel>
  <ProgressValue />
  <ProgressTrack>
    <ProgressIndicator className="bg-red-500 dark:bg-red-400" />
  </ProgressTrack>
</Progress>
```

### Size Variants

```tsx
// Small
<Progress value={65}>
  <ProgressLabel>Small</ProgressLabel>
  <ProgressValue />
  <ProgressTrack className="h-1">
    <ProgressIndicator />
  </ProgressTrack>
</Progress>

// Default
<Progress value={65}>
  <ProgressLabel>Default</ProgressLabel>
  <ProgressValue />
  <ProgressTrack className="h-1.5">
    <ProgressIndicator />
  </ProgressTrack>
</Progress>

// Large
<Progress value={65}>
  <ProgressLabel>Large</ProgressLabel>
  <ProgressValue />
  <ProgressTrack className="h-3">
    <ProgressIndicator />
  </ProgressTrack>
</Progress>
```

### Indeterminate State

For tasks with unknown duration:

```tsx
<Progress value={null}>
  <ProgressLabel>Processing...</ProgressLabel>
  <ProgressTrack>
    <ProgressIndicator className="w-1/3 animate-pulse" />
  </ProgressTrack>
</Progress>
```

### API Usage Dashboard Context

```tsx
<div className="border-border bg-card rounded-lg border p-5">
  <div className="mb-4 flex items-start justify-between">
    <div className="flex items-center gap-3">
      <div className="bg-primary/10 text-primary rounded-md p-2">
        <ActivityIcon className="size-5" />
      </div>
      <div>
        <p className="font-medium">Monthly API Calls</p>
        <p className="text-muted-foreground text-sm">
          Standard Plan: 1M requests/month
        </p>
      </div>
    </div>
    <span className="rounded-full bg-green-500/10 px-2.5 py-1 text-xs font-medium text-green-600">
      Healthy
    </span>
  </div>

  <Progress value={45}>
    <ProgressLabel>API Requests</ProgressLabel>
    <ProgressValue />
    <ProgressTrack>
      <ProgressIndicator className="bg-green-500 dark:bg-green-400" />
    </ProgressTrack>
  </Progress>

  <p className="text-muted-foreground mt-2 text-sm">
    450,000 of 1,000,000 requests used
  </p>
</div>
```

## Accessibility

- Uses `role="progressbar"` with proper ARIA attributes
- Announces progress value and label to screen readers
- Supports `aria-valuetext` for custom accessible labels
- `getAriaValueText` prop allows dynamic accessible descriptions
- Indeterminate state properly announced when `value={null}`

## Related

- **Skeleton** - Loading placeholders for content
- **Spinner** - Circular loading indicator
- **Badge** - Status indicators with semantic colors

---

# RadioGroup

Mutually exclusive selection from a group of options.

## Import

```tsx
import { RadioGroup, RadioGroupItem } from "@neynar/ui/radio-group"
```

## Anatomy

```tsx
<RadioGroup value={value} onValueChange={setValue}>
  <RadioGroupItem value="option-1" id="option-1" />
  <RadioGroupItem value="option-2" id="option-2" />
</RadioGroup>
```

## Components

| Component | Description |
|-----------|-------------|
| RadioGroup | Container that manages state for radio items |
| RadioGroupItem | Individual radio button with automatic indicator |

## Props

### RadioGroup

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | unknown | - | Controlled selected value |
| onValueChange | (value: unknown) => void | - | Called when selection changes |
| defaultValue | unknown | - | Uncontrolled initial value |
| name | string | - | Form field name for submission |
| disabled | boolean | false | Disable all radio items |
| readOnly | boolean | false | Prevent selection changes |
| required | boolean | false | Mark as required for forms |
| inputRef | Ref\<HTMLInputElement\> | - | Ref to hidden input element |
| className | string | - | Additional CSS classes |

### RadioGroupItem

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | - | Value for this radio option |
| id | string | - | HTML id for label association |
| disabled | boolean | false | Disable this specific item |
| aria-invalid | boolean | - | Mark as invalid (error state) |
| className | string | - | Additional CSS classes |

The RadioGroupItem automatically renders a checked indicator (filled circle) when selected.

## Data Attributes

### RadioGroup

| Attribute | When Present |
|-----------|--------------|
| data-disabled | Group is disabled |

### RadioGroupItem

| Attribute | When Present |
|-----------|--------------|
| data-checked | Item is selected |
| data-unchecked | Item is not selected |
| data-disabled | Item is disabled |

## Examples

### Basic Usage

```tsx
function NotificationPreferences() {
  const [method, setMethod] = useState("email")

  return (
    <RadioGroup value={method} onValueChange={(v) => setMethod(v as string)}>
      <div className="flex items-center space-x-2">
        <RadioGroupItem value="email" id="email" />
        <Label htmlFor="email">Email</Label>
      </div>
      <div className="flex items-center space-x-2">
        <RadioGroupItem value="sms" id="sms" />
        <Label htmlFor="sms">SMS</Label>
      </div>
      <div className="flex items-center space-x-2">
        <RadioGroupItem value="push" id="push" />
        <Label htmlFor="push">Push Notifications</Label>
      </div>
    </RadioGroup>
  )
}
```

### Uncontrolled with Default Value

```tsx
<RadioGroup defaultValue="standard" name="shipping">
  <div className="flex items-center space-x-2">
    <RadioGroupItem value="standard" id="standard" />
    <Label htmlFor="standard">Standard Shipping</Label>
  </div>
  <div className="flex items-center space-x-2">
    <RadioGroupItem value="express" id="express" />
    <Label htmlFor="express">Express Shipping</Label>
  </div>
</RadioGroup>
```

### With Descriptions

```tsx
<RadioGroup defaultValue="mainnet">
  <div className="flex items-start space-x-3">
    <RadioGroupItem value="mainnet" id="mainnet" className="mt-0.5" />
    <div className="flex-1">
      <Label htmlFor="mainnet" className="font-medium">Mainnet</Label>
      <p className="text-muted-foreground text-sm">
        Production-ready network with real users and data.
      </p>
    </div>
  </div>
  <div className="flex items-start space-x-3">
    <RadioGroupItem value="testnet" id="testnet" className="mt-0.5" />
    <div className="flex-1">
      <Label htmlFor="testnet" className="font-medium">Testnet</Label>
      <p className="text-muted-foreground text-sm">
        Testing environment for development.
      </p>
    </div>
  </div>
</RadioGroup>
```

### Error State

```tsx
function FormField() {
  const [value, setValue] = useState("")
  const [showError, setShowError] = useState(false)

  return (
    <div className="space-y-2">
      <RadioGroup
        value={value}
        onValueChange={(v) => {
          setValue(v as string)
          setShowError(false)
        }}
        aria-invalid={showError}
      >
        <div className="flex items-center space-x-2">
          <RadioGroupItem value="yes" id="yes" aria-invalid={showError} />
          <Label htmlFor="yes">Yes</Label>
        </div>
        <div className="flex items-center space-x-2">
          <RadioGroupItem value="no" id="no" aria-invalid={showError} />
          <Label htmlFor="no">No</Label>
        </div>
      </RadioGroup>
      {showError && (
        <p className="text-destructive text-sm">Please make a selection.</p>
      )}
    </div>
  )
}
```

### Disabled Group

```tsx
<RadioGroup disabled defaultValue="option-1">
  <div className="flex items-center space-x-2">
    <RadioGroupItem value="option-1" id="option-1" />
    <Label htmlFor="option-1">Option 1</Label>
  </div>
  <div className="flex items-center space-x-2">
    <RadioGroupItem value="option-2" id="option-2" />
    <Label htmlFor="option-2">Option 2</Label>
  </div>
</RadioGroup>
```

### Individual Disabled Items

```tsx
<RadioGroup defaultValue="available-1">
  <div className="flex items-center space-x-2">
    <RadioGroupItem value="available-1" id="available-1" />
    <Label htmlFor="available-1">Available Option</Label>
  </div>
  <div className="flex items-center space-x-2">
    <RadioGroupItem value="sold-out" id="sold-out" disabled />
    <Label htmlFor="sold-out">Sold Out</Label>
  </div>
  <div className="flex items-center space-x-2">
    <RadioGroupItem value="available-2" id="available-2" />
    <Label htmlFor="available-2">Another Available</Label>
  </div>
</RadioGroup>
```

### Card-Based Layout

```tsx
<RadioGroup defaultValue="pro">
  <div className="grid gap-4 sm:grid-cols-3">
    <label
      htmlFor="free"
      className="hover:border-primary/50 border-border flex cursor-pointer flex-col gap-3 rounded-lg border p-4"
    >
      <RadioGroupItem value="free" id="free" />
      <div>
        <p className="font-semibold">Free Plan</p>
        <p className="text-muted-foreground text-sm">Perfect for testing</p>
      </div>
    </label>

    <label
      htmlFor="pro"
      className="hover:border-primary/50 border-primary border-2 flex cursor-pointer flex-col gap-3 rounded-lg p-4"
    >
      <RadioGroupItem value="pro" id="pro" />
      <div>
        <p className="font-semibold">Pro Plan</p>
        <p className="text-muted-foreground text-sm">For production apps</p>
      </div>
    </label>

    <label
      htmlFor="enterprise"
      className="hover:border-primary/50 border-border flex cursor-pointer flex-col gap-3 rounded-lg border p-4"
    >
      <RadioGroupItem value="enterprise" id="enterprise" />
      <div>
        <p className="font-semibold">Enterprise</p>
        <p className="text-muted-foreground text-sm">Custom solutions</p>
      </div>
    </label>
  </div>
</RadioGroup>
```

## Keyboard

| Key | Action |
|-----|--------|
| Tab | Focus next radio item |
| Shift+Tab | Focus previous radio item |
| Space | Select focused item |
| Arrow Down/Right | Select next item |
| Arrow Up/Left | Select previous item |

## Accessibility

- Each RadioGroupItem should have a unique `id` and paired `<Label htmlFor={id}>`
- Uses `role="radiogroup"` and `role="radio"` with proper ARIA attributes
- Manages focus with roving tabindex for keyboard navigation
- Error states use `aria-invalid` for screen reader announcements
- Disabled state prevents interaction and is announced to assistive technologies

## Related

- [Label](./label.llm.md) - For labeling radio items
- [Checkbox](./checkbox.llm.md) - For multi-select options
- [Select](./select.llm.md) - For dropdown selections

---

# Resizable

Create resizable panel layouts with draggable handles for complex dashboard, editor, and split-view interfaces.

## Import

```tsx
import {
  ResizablePanelGroup,
  ResizablePanel,
  ResizableHandle,
} from "@neynar/ui/resizable"
```

## Anatomy

```tsx
<ResizablePanelGroup orientation="horizontal">
  <ResizablePanel defaultSize={50}>
    Content 1
  </ResizablePanel>
  <ResizableHandle />
  <ResizablePanel defaultSize={50}>
    Content 2
  </ResizablePanel>
</ResizablePanelGroup>
```

## Components

| Component | Description |
|-----------|-------------|
| ResizablePanelGroup | Root container that manages layout orientation and panel resizing |
| ResizablePanel | Individual resizable panel with size constraints and collapse behavior |
| ResizableHandle | Draggable separator between panels with optional visible grip |

## Props

### ResizablePanelGroup

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| orientation | "horizontal" \| "vertical" | "horizontal" | Layout direction for panels |
| id | string | - | Unique ID for localStorage persistence of panel sizes |
| defaultLayout | number[] | - | Initial panel sizes as percentages |
| onLayoutChange | (layout: Record\<string, number\>) => void | - | Called when panel sizes change |
| disabled | boolean | false | Disable all resizing |
| className | string | - | Additional CSS classes |

### ResizablePanel

All panels inherit props from react-resizable-panels Panel component.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| id | string | - | Panel identifier (required for persistence) |
| defaultSize | number | - | Initial size as percentage of parent group |
| minSize | number | 0 | Minimum size as percentage |
| maxSize | number | 100 | Maximum size as percentage |
| collapsible | boolean | false | Allow panel to collapse below minSize |
| collapsedSize | number | 0 | Size when collapsed |
| onResize | (size: { asPercentage: number; inPixels: number }) => void | - | Called when panel is resized |
| className | string | - | Additional CSS classes |

### ResizableHandle

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| withHandle | boolean | false | Display visible grip indicator |
| className | string | - | Additional CSS classes |

## Data Attributes

| Attribute | When Present | Applied To |
|-----------|--------------|------------|
| data-slot="resizable-panel-group" | Always | ResizablePanelGroup |
| data-slot="resizable-panel" | Always | ResizablePanel |
| data-slot="resizable-handle" | Always | ResizableHandle |
| aria-orientation | Inherits from parent group | All components |

## Examples

### Basic Horizontal Layout

```tsx
<ResizablePanelGroup orientation="horizontal">
  <ResizablePanel defaultSize={40}>
    <div className="p-4">Sidebar</div>
  </ResizablePanel>
  <ResizableHandle />
  <ResizablePanel defaultSize={60}>
    <div className="p-4">Main Content</div>
  </ResizablePanel>
</ResizablePanelGroup>
```

### Vertical Split with Handle

```tsx
<ResizablePanelGroup orientation="vertical">
  <ResizablePanel defaultSize={70}>
    <div className="p-4">Editor</div>
  </ResizablePanel>
  <ResizableHandle withHandle />
  <ResizablePanel defaultSize={30}>
    <div className="p-4">Terminal Output</div>
  </ResizablePanel>
</ResizablePanelGroup>
```

### Three Panels with Constraints

```tsx
<ResizablePanelGroup orientation="horizontal">
  <ResizablePanel defaultSize={25} minSize={15} maxSize={40}>
    <div className="p-4">Navigation</div>
  </ResizablePanel>
  <ResizableHandle withHandle />
  <ResizablePanel defaultSize={50} minSize={30}>
    <div className="p-4">Content</div>
  </ResizablePanel>
  <ResizableHandle withHandle />
  <ResizablePanel defaultSize={25} minSize={15} maxSize={40}>
    <div className="p-4">Inspector</div>
  </ResizablePanel>
</ResizablePanelGroup>
```

### Nested Layouts (Editor + Terminal)

```tsx
<ResizablePanelGroup orientation="horizontal">
  <ResizablePanel defaultSize={20} minSize={15}>
    <div className="p-4">File Explorer</div>
  </ResizablePanel>
  <ResizableHandle withHandle />
  <ResizablePanel defaultSize={80}>
    <ResizablePanelGroup orientation="vertical">
      <ResizablePanel defaultSize={70}>
        <div className="p-4">Code Editor</div>
      </ResizablePanel>
      <ResizableHandle withHandle />
      <ResizablePanel defaultSize={30} minSize={20}>
        <div className="p-4">Terminal</div>
      </ResizablePanel>
    </ResizablePanelGroup>
  </ResizablePanel>
</ResizablePanelGroup>
```

### Persisted Layout

```tsx
function Dashboard() {
  return (
    <ResizablePanelGroup
      id="dashboard-layout"
      orientation="horizontal"
    >
      <ResizablePanel id="sidebar" defaultSize={25}>
        <div className="p-4">Sidebar</div>
      </ResizablePanel>
      <ResizableHandle />
      <ResizablePanel id="main" defaultSize={75}>
        <div className="p-4">Main Content</div>
      </ResizablePanel>
    </ResizablePanelGroup>
  )
}
```

### Controlled with Callbacks

```tsx
function MonitoredPanels() {
  const [sizes, setSizes] = useState({ left: 30, right: 70 })

  return (
    <ResizablePanelGroup
      orientation="horizontal"
      onLayoutChange={(layout) => {
        setSizes({
          left: layout.left || 0,
          right: layout.right || 0
        })
      }}
    >
      <ResizablePanel id="left" defaultSize={30}>
        <div className="p-4">Left: {sizes.left.toFixed(1)}%</div>
      </ResizablePanel>
      <ResizableHandle withHandle />
      <ResizablePanel id="right">
        <div className="p-4">Right: {sizes.right.toFixed(1)}%</div>
      </ResizablePanel>
    </ResizablePanelGroup>
  )
}
```

## Keyboard

| Key | Action |
|-----|--------|
| Tab | Focus next handle |
| Shift + Tab | Focus previous handle |
| Arrow Keys | Resize panels (direction depends on orientation) |
| Enter | Expand collapsed panel |
| Home | Resize to minimum |
| End | Resize to maximum |

## Accessibility

- Resize handles are keyboard-navigable with proper focus states
- ARIA attributes automatically reflect orientation and state
- Supports keyboard resizing via arrow keys
- Focus ring visible on keyboard navigation

## Related

- [Collapsible](./collapsible.llm.md) - For simpler expand/collapse UI
- [Tabs](./tabs.llm.md) - Alternative to split views
- [Card](./card.llm.md) - Static panel container

---

# ScrollArea Component

## Overview
The ScrollArea component provides a customizable scrollable container with styled scrollbars, built on Base UI's ScrollArea primitive. It offers automatic overflow detection, custom scrollbar styling, and support for both vertical and horizontal scrolling.

## Component Exports

### ScrollArea
Main scrollable container component with custom-styled scrollbars.

**Props**: `ScrollAreaProps` (extends `ScrollAreaPrimitive.Root.Props`)
- `className?: string` - Additional CSS classes to apply
- `children: React.ReactNode` - Content to be rendered within the scrollable area
- All Base UI ScrollArea.Root props

**Key Features**:
- Automatic overflow detection
- Custom-styled scrollbars with border-based color
- Focus ring on keyboard navigation
- Inherits border radius from parent styling
- Smooth transitions for focus states

### ScrollBar
Custom scrollbar component that can be used independently or within ScrollArea.

**Props**: `ScrollBarProps` (extends `ScrollAreaPrimitive.Scrollbar.Props`)
- `className?: string` - Additional CSS classes to apply
- `orientation?: "vertical" | "horizontal"` - Scrollbar orientation (default: "vertical")
- All Base UI ScrollArea.Scrollbar props

**Key Features**:
- Draggable thumb for scroll control
- Adaptive sizing based on orientation (2.5 units height/width)
- Touch-optimized for mobile devices
- Rounded scrollbar thumb

## Usage Examples

### Basic Vertical Scrolling
```tsx
import { ScrollArea } from "@neynar/ui/scroll-area"

function ContentList() {
  return (
    <ScrollArea className="h-72 w-48 rounded-md border p-4">
      <div className="space-y-4">
        {items.map((item) => (
          <div key={item.id}>{item.content}</div>
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Horizontal Scrolling
```tsx
import { ScrollArea } from "@neynar/ui/scroll-area"

function HorizontalGallery() {
  return (
    <ScrollArea className="w-96 whitespace-nowrap rounded-md border">
      <div className="flex gap-4 p-4">
        {images.map((img) => (
          <img key={img.id} src={img.url} alt={img.alt} className="h-40 w-auto" />
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Chat-Style Scrolling
```tsx
import { ScrollArea } from "@neynar/ui/scroll-area"

function ChatMessages() {
  return (
    <ScrollArea className="h-[600px] rounded-lg border p-4">
      <div className="flex flex-col gap-2">
        {messages.map((message) => (
          <div key={message.id} className="rounded-lg bg-muted p-3">
            <p className="text-sm font-medium">{message.author}</p>
            <p className="text-sm">{message.content}</p>
          </div>
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Sidebar Navigation
```tsx
import { ScrollArea } from "@neynar/ui/scroll-area"

function Sidebar() {
  return (
    <aside className="w-64 border-r">
      <ScrollArea className="h-screen p-4">
        <nav className="space-y-2">
          {navItems.map((item) => (
            <a
              key={item.href}
              href={item.href}
              className="block rounded-md px-3 py-2 hover:bg-accent"
            >
              {item.label}
            </a>
          ))}
        </nav>
      </ScrollArea>
    </aside>
  )
}
```

### Custom Scrollbar Configuration
```tsx
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
import { ScrollBar } from "@neynar/ui/scroll-area"

function CustomScrollArea({ children }: { children: React.ReactNode }) {
  return (
    <ScrollAreaPrimitive.Root className="relative h-96 rounded-md border">
      <ScrollAreaPrimitive.Viewport className="size-full p-4">
        {children}
      </ScrollAreaPrimitive.Viewport>
      <ScrollBar orientation="horizontal" className="bg-muted/50" />
      <ScrollAreaPrimitive.Corner />
    </ScrollAreaPrimitive.Root>
  )
}
```

## Component Structure

```
ScrollArea (Root container)
├── Viewport (content wrapper with focus ring)
│   └── {children}
├── ScrollBar (vertical by default)
│   └── Thumb (draggable scrollbar handle)
└── Corner (intersection of scrollbars)
```

## Styling and Theming

### Default Styles
- **Root**: `relative` positioning for scrollbar placement
- **Viewport**: Full size with inherited border radius, focus ring on keyboard navigation
- **ScrollBar**: 2.5 units width/height depending on orientation, touch-optimized
- **Thumb**: Rounded, uses `border` color from theme

### Customization Points
- Apply height/width constraints via `className` on ScrollArea
- Customize scrollbar appearance via `className` on ScrollBar
- Border radius is inherited from parent container
- Colors follow the theme's `border` color token

### Data Attributes
- `data-slot="scroll-area"` - Root container
- `data-slot="scroll-area-viewport"` - Content viewport
- `data-slot="scroll-area-scrollbar"` - Scrollbar container
- `data-slot="scroll-area-thumb"` - Scrollbar thumb
- `data-orientation="vertical|horizontal"` - Scrollbar orientation state

## Accessibility

### Keyboard Navigation
- **Arrow Keys**: Scroll viewport when focused
- **Page Up/Down**: Scroll by page
- **Home/End**: Scroll to start/end
- **Tab**: Focus scrollbar (visible focus ring)

### Screen Reader Support
- Scrollable regions are properly announced
- Scrollbar controls are keyboard accessible
- Focus management follows standard patterns

### Touch Gestures
- Scrollbar is touch-optimized with `touch-none` for precise dragging
- Natural swipe scrolling on viewport

## Common Patterns

### Fixed Height Content
```tsx
<ScrollArea className="h-96 rounded-md border">
  {/* Content that exceeds 384px height will scroll */}
</ScrollArea>
```

### Full Viewport Height
```tsx
<ScrollArea className="h-screen">
  {/* Scrolls within viewport height */}
</ScrollArea>
```

### Responsive Heights
```tsx
<ScrollArea className="h-[50vh] md:h-[70vh] rounded-md border">
  {/* Adapts to viewport size */}
</ScrollArea>
```

### Preventing Text Selection During Scroll
The scrollbar uses `select-none` to prevent text selection while dragging, but content remains selectable.

## Base UI Integration

This component wraps Base UI's ScrollArea primitive:
- **Root**: Container with scroll detection
- **Viewport**: Scrollable content area
- **Scrollbar**: Custom scrollbar UI
- **Thumb**: Draggable scroll indicator
- **Corner**: Intersection element for dual scrollbars

Base UI handles:
- Overflow detection and scrollbar visibility
- Scroll position synchronization
- Touch and mouse interactions
- Accessibility features

## Implementation Notes

### Why Not Native Scrollbars?
- Consistent cross-browser appearance
- Better theming and customization
- Touch-optimized interactions
- Accessible keyboard controls

### Performance Considerations
- Viewport uses hardware-accelerated transitions
- Scrollbar only renders when content overflows
- Efficient re-renders with Base UI primitives

### Browser Compatibility
- Modern browsers with CSS Grid and Flexbox support
- Touch events for mobile devices
- Fallback to native scrolling if JavaScript disabled

## Comparison with Similar Components

### ScrollArea vs. Native Overflow
- **ScrollArea**: Custom styled scrollbars, consistent appearance
- **Native**: Browser-dependent styling, system scrollbars

### When to Use ScrollArea
- Consistent design across browsers and platforms
- Custom scrollbar styling requirements
- Need for touch-optimized scrolling
- Keyboard navigation requirements

### When to Use Native Scrolling
- Simple content with minimal styling needs
- Performance-critical rendering (native is slightly faster)
- Desire for OS-native scrollbar appearance

## Migration from Radix UI

If migrating from Radix UI ScrollArea:

```tsx
// Before (Radix UI)
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"

<ScrollAreaPrimitive.Root>
  <ScrollAreaPrimitive.Viewport>
    {children}
  </ScrollAreaPrimitive.Viewport>
  <ScrollAreaPrimitive.Scrollbar orientation="vertical">
    <ScrollAreaPrimitive.Thumb />
  </ScrollAreaPrimitive.Scrollbar>
</ScrollAreaPrimitive.Root>

// After (Base UI)
import { ScrollArea } from "@neynar/ui/scroll-area"

<ScrollArea className="h-96">
  {children}
</ScrollArea>
```

Key differences:
- Base UI uses simpler composition
- Automatic scrollbar rendering
- Focus ring styling built-in
- Data attributes follow `data-slot` pattern

---

# Select

Dropdown component for selecting a single value from a list of options with grouping, icons, and keyboard navigation.

## Import

```tsx
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
} from "@neynar/ui/select"
```

## Anatomy

```tsx
<Select>
  <SelectTrigger>
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectLabel>Group Label</SelectLabel>
      <SelectItem value="1">Option 1</SelectItem>
      <SelectItem value="2">Option 2</SelectItem>
    </SelectGroup>
    <SelectSeparator />
    <SelectGroup>
      <SelectLabel>Another Group</SelectLabel>
      <SelectItem value="3">Option 3</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>
```

## Components

| Component | Description |
|-----------|-------------|
| Select | Root container, manages selection state and keyboard navigation |
| SelectTrigger | Button that opens the dropdown, includes chevron icon automatically |
| SelectValue | Displays the selected item's value or placeholder |
| SelectContent | Dropdown popup with automatic portal, positioning, and scroll buttons |
| SelectGroup | Groups related items under a label |
| SelectLabel | Non-selectable label for a group |
| SelectItem | Individual selectable option with automatic check indicator |
| SelectSeparator | Visual divider between groups |
| SelectScrollUpButton | Scroll indicator at top (automatic) |
| SelectScrollDownButton | Scroll indicator at bottom (automatic) |

## Props

### Select

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | - | Controlled selected value |
| defaultValue | string | - | Uncontrolled default value |
| onValueChange | (value: string \| null) => void | - | Called when selection changes |
| disabled | boolean | false | Disables the entire select |
| name | string | - | Name attribute for form submission |

### SelectTrigger

Button that opens the dropdown. Automatically includes chevron icon.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| size | "sm" \| "default" | "default" | Trigger button size |
| className | string | - | Additional CSS classes |

### SelectValue

Displays the selected value or placeholder. Automatically shows selected item text.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| placeholder | string | - | Text shown when no value selected |
| className | string | - | Additional CSS classes |

### SelectContent

Automatically renders in a portal with backdrop overlay, positioning, and scroll indicators.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| side | "top" \| "right" \| "bottom" \| "left" | "bottom" | Preferred side to position relative to trigger |
| sideOffset | number | 4 | Distance from trigger in pixels |
| align | "start" \| "center" \| "end" | "center" | Alignment relative to trigger |
| alignOffset | number | 0 | Offset from aligned position |
| alignItemWithTrigger | boolean | true | Whether to align selected item with trigger |
| className | string | - | Additional CSS classes |

### SelectItem

Individual option. Automatically shows check icon when selected.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | - | **Required.** Unique value for this option |
| disabled | boolean | false | Disables this specific item |
| label | string | - | Label for keyboard text navigation (defaults to text content) |
| className | string | - | Additional CSS classes |

### SelectGroup

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |

### SelectLabel

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |

### SelectSeparator

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |

## Data Attributes (for styling)

### SelectTrigger

| Attribute | When Present |
|-----------|--------------|
| data-size | Always present with value "sm" or "default" |
| data-placeholder | No value selected |
| aria-invalid | Invalid state (for forms) |

### SelectItem

| Attribute | When Present |
|-----------|--------------|
| data-selected | Item is currently selected |
| data-highlighted | Item is keyboard-highlighted |
| data-disabled | Item is disabled |

### SelectContent

| Attribute | When Present |
|-----------|--------------|
| data-open | Popup is open |
| data-closed | Popup is closed |
| data-side | Position side (top/right/bottom/left) |

## Examples

### Basic Select

```tsx
<Select defaultValue="option2">
  <SelectTrigger className="w-[200px]">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="option1">Option 1</SelectItem>
    <SelectItem value="option2">Option 2</SelectItem>
    <SelectItem value="option3">Option 3</SelectItem>
  </SelectContent>
</Select>
```

### Controlled with State

```tsx
const [region, setRegion] = useState("us-east-1")

<Select value={region} onValueChange={(v) => v && setRegion(v)}>
  <SelectTrigger className="w-full">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="us-east-1">US East (N. Virginia)</SelectItem>
    <SelectItem value="us-west-2">US West (Oregon)</SelectItem>
    <SelectItem value="eu-west-1">EU (Ireland)</SelectItem>
  </SelectContent>
</Select>
```

### With Icons

```tsx
<Select defaultValue="admin">
  <SelectTrigger className="w-[280px]">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="admin">
      <SettingsIcon data-icon="inline-start" />
      Administrator
    </SelectItem>
    <SelectItem value="user">
      <UserIcon data-icon="inline-start" />
      Regular User
    </SelectItem>
  </SelectContent>
</Select>
```

### Grouped Options

```tsx
<Select defaultValue="react">
  <SelectTrigger className="w-[280px]">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectLabel>Frontend</SelectLabel>
      <SelectItem value="react">React</SelectItem>
      <SelectItem value="vue">Vue</SelectItem>
    </SelectGroup>
    <SelectSeparator />
    <SelectGroup>
      <SelectLabel>Backend</SelectLabel>
      <SelectItem value="node">Node.js</SelectItem>
      <SelectItem value="python">Python</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>
```

### Form Integration

```tsx
<div className="space-y-2">
  <label htmlFor="country" className="text-sm font-medium">
    Country
  </label>
  <Select defaultValue="us">
    <SelectTrigger id="country">
      <SelectValue />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="us">United States</SelectItem>
      <SelectItem value="uk">United Kingdom</SelectItem>
      <SelectItem value="ca">Canada</SelectItem>
    </SelectContent>
  </Select>
  <p className="text-muted-foreground text-xs">
    Select your country of residence.
  </p>
</div>
```

### Small Size

```tsx
<Select defaultValue="sm">
  <SelectTrigger size="sm" className="w-[180px]">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="sm">Small option</SelectItem>
    <SelectItem value="sm2">Another small</SelectItem>
  </SelectContent>
</Select>
```

### Disabled States

```tsx
// Disabled select
<Select defaultValue="option1" disabled>
  <SelectTrigger className="w-[200px]">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="option1">Option 1</SelectItem>
  </SelectContent>
</Select>

// Disabled items
<Select defaultValue="available">
  <SelectTrigger className="w-[200px]">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="available">Available</SelectItem>
    <SelectItem value="unavailable" disabled>
      Unavailable
    </SelectItem>
  </SelectContent>
</Select>
```

### Placeholder

```tsx
<Select>
  <SelectTrigger className="w-[280px]">
    <SelectValue placeholder="Select a fruit" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="apple">Apple</SelectItem>
    <SelectItem value="banana">Banana</SelectItem>
  </SelectContent>
</Select>
```

## Keyboard

| Key | Action |
|-----|--------|
| Space / Enter | Open dropdown (when closed) |
| Space / Enter | Select highlighted item (when open) |
| Escape | Close dropdown |
| ArrowDown | Highlight next item |
| ArrowUp | Highlight previous item |
| Home | Highlight first item |
| End | Highlight last item |
| Type characters | Jump to item starting with typed text |
| Tab | Close and move to next focusable element |

## Accessibility

- Uses `role="combobox"` on trigger with proper ARIA attributes
- Implements `aria-expanded`, `aria-controls`, and `aria-activedescendant`
- Selected items marked with `aria-selected="true"`
- Disabled items use `aria-disabled="true"`
- Keyboard navigation follows ARIA combobox pattern
- Focus management returns to trigger on close
- Screen readers announce selection changes

## Related

- [Combobox](/components/combobox) - Searchable select with filtering
- [Radio Group](/components/radio-group) - Single selection from visible options
- [Command](/components/command) - Command palette with search

---

# Separator

Visual divider for separating content sections with support for horizontal and vertical orientations.

## Import

```tsx
import { Separator } from "@neynar/ui/separator"
```

## Anatomy

```tsx
<Separator />
```

## Props

### Separator

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| orientation | "horizontal" \| "vertical" | "horizontal" | Direction of the separator |
| className | string | - | Additional CSS classes |
| render | ReactElement \| function | - | Custom render function or element |

All Base UI Separator props are supported via SeparatorProps type.

## Data Attributes

| Attribute | When Present |
|-----------|--------------|
| data-slot | Always set to "separator" |
| data-orientation | Set to "horizontal" or "vertical" |

## Styling

The Separator uses data attributes for responsive sizing:

- **Horizontal**: `data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full`
- **Vertical**: `data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch`

For vertical separators, you typically need to set an explicit height:

```tsx
<Separator orientation="vertical" className="h-6" />
```

## Examples

### Basic Horizontal Separator

```tsx
<div className="space-y-4">
  <p>First section</p>
  <Separator />
  <p>Second section</p>
</div>
```

### Vertical Separator in Toolbar

```tsx
<div className="flex items-center gap-3">
  <Button>Bold</Button>
  <Button>Italic</Button>
  <Separator orientation="vertical" className="h-6" />
  <Button>Link</Button>
  <Button>Image</Button>
</div>
```

### In List Items

```tsx
<div className="border rounded-lg space-y-0">
  <div className="p-4">
    <p className="font-medium">Dashboard</p>
  </div>
  <Separator />
  <div className="p-4">
    <p className="font-medium">API Keys</p>
  </div>
  <Separator />
  <div className="p-4">
    <p className="font-medium">Settings</p>
  </div>
</div>
```

### Inline Metadata with Vertical Separators

```tsx
<div className="flex items-center gap-2 text-sm">
  <span>Status: 200 OK</span>
  <Separator orientation="vertical" className="h-4" />
  <span>Latency: 142ms</span>
  <Separator orientation="vertical" className="h-4" />
  <span>2 minutes ago</span>
</div>
```

### Custom Spacing

Control spacing with margin utilities:

```tsx
<div>
  <p>Tight spacing</p>
  <Separator className="my-2" />
  <p>Default spacing</p>
  <Separator className="my-4" />
  <p>Loose spacing</p>
  <Separator className="my-6" />
</div>
```

## Accessibility

- Renders as a `<div>` with proper ARIA role
- Accessible to screen readers
- Provides semantic separation between content sections
- Works with keyboard navigation (doesn't receive focus)

## Related

- [Card](./card.llm.md) - Often uses Separator to divide card sections
- [Dialog](./dialog.llm.md) - May use Separator between header/body/footer
- [Dropdown Menu](./dropdown-menu.llm.md) - Uses Separator to group menu items

---

# Sheet

Slide-in panels that overlay the page from any edge, ideal for mobile navigation, filters, and contextual content.

## Import

```tsx
import {
  Sheet,
  SheetTrigger,
  SheetContent,
  SheetHeader,
  SheetFooter,
  SheetTitle,
  SheetDescription,
  SheetClose,
} from "@neynar/ui/sheet"
```

## Anatomy

```tsx
<Sheet>
  <SheetTrigger />
  <SheetContent>
    <SheetHeader>
      <SheetTitle />
      <SheetDescription />
    </SheetHeader>
    {/* Content */}
    <SheetFooter>
      <SheetClose />
    </SheetFooter>
  </SheetContent>
</Sheet>
```

## Components

| Component | Description |
|-----------|-------------|
| Sheet | Root container, manages open/closed state |
| SheetTrigger | Button that opens the sheet |
| SheetContent | Main panel with automatic portal, overlay, and animations |
| SheetHeader | Header section for title and description |
| SheetFooter | Footer section for action buttons (auto-sticks to bottom) |
| SheetTitle | Accessible title (linked to sheet for screen readers) |
| SheetDescription | Accessible description (provides context) |
| SheetClose | Button that closes the sheet |

## Props

### Sheet

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | - | Controlled open state |
| defaultOpen | boolean | - | Uncontrolled default open state |
| onOpenChange | (open: boolean) => void | - | Callback when open state changes |
| modal | boolean | true | Whether to block interaction with page content |

### SheetContent

Automatically renders in a portal with overlay backdrop.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| side | "top" \| "right" \| "bottom" \| "left" | "right" | Which edge the sheet slides in from |
| showCloseButton | boolean | true | Show close button (X) in top-right corner |
| className | string | - | Additional CSS classes |
| initialFocus | HTMLElement \| (() => HTMLElement) | - | Element to focus when sheet opens |
| finalFocus | HTMLElement \| (() => HTMLElement) | - | Element to focus when sheet closes |

### SheetTrigger / SheetClose

Both support the `render` prop pattern:

```tsx
<SheetTrigger render={<Button variant="outline" />}>
  Open Sheet
</SheetTrigger>

<SheetClose render={<Button variant="ghost" />}>
  Close
</SheetClose>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| render | React.ReactElement | - | Custom element to render as trigger/close |
| className | string | - | Additional CSS classes |

### SheetHeader / SheetFooter

Both are simple `<div>` wrappers with consistent spacing.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes |

## Data Attributes

Available for styling via CSS:

| Attribute | When Present | Applied To |
|-----------|--------------|------------|
| data-open | Sheet is open | SheetContent, SheetOverlay |
| data-closed | Sheet is closed | SheetContent, SheetOverlay |
| data-starting-style | During open animation start | SheetContent, SheetOverlay |
| data-ending-style | During close animation end | SheetContent, SheetOverlay |
| data-side | Always (value: top/right/bottom/left) | SheetContent |

## Examples

### Basic Usage

```tsx
<Sheet>
  <SheetTrigger render={<Button variant="outline" />}>
    Open Settings
  </SheetTrigger>
  <SheetContent>
    <SheetHeader>
      <SheetTitle>Settings</SheetTitle>
      <SheetDescription>
        Manage your account settings and preferences
      </SheetDescription>
    </SheetHeader>
    <div className="py-6">
      {/* Settings content */}
    </div>
  </SheetContent>
</Sheet>
```

### Mobile Navigation (Left Side)

```tsx
<Sheet>
  <SheetTrigger render={<Button variant="ghost" size="icon" />}>
    <MenuIcon />
  </SheetTrigger>
  <SheetContent side="left" className="p-0">
    <SheetHeader className="border-b p-6">
      <SheetTitle>Neynar Dashboard</SheetTitle>
      <SheetDescription>
        Navigate your Farcaster analytics
      </SheetDescription>
    </SheetHeader>
    <nav className="flex-1 space-y-1 p-4">
      {/* Navigation items */}
    </nav>
  </SheetContent>
</Sheet>
```

### Filter Panel with Footer Actions

```tsx
<Sheet>
  <SheetTrigger render={<Button variant="outline" />}>
    <FilterIcon data-icon="inline-start" />
    Filters
  </SheetTrigger>
  <SheetContent side="right">
    <SheetHeader>
      <SheetTitle>Filter Users</SheetTitle>
      <SheetDescription>
        Refine your user list with advanced filters
      </SheetDescription>
    </SheetHeader>

    <div className="space-y-6 p-4">
      <div className="space-y-2">
        <Label htmlFor="search">Search</Label>
        <Input id="search" placeholder="Search by username" />
      </div>
      {/* More filters */}
    </div>

    <SheetFooter>
      <SheetClose render={<Button variant="outline" />}>
        Cancel
      </SheetClose>
      <Button>Apply Filters</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>
```

### Controlled State

```tsx
function ProfileEditor() {
  const [open, setOpen] = useState(false)

  const handleSave = async () => {
    await saveProfile()
    setOpen(false) // Close after saving
  }

  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger render={<Button />}>
        Edit Profile
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Edit Profile</SheetTitle>
          <SheetDescription>
            Make changes to your profile
          </SheetDescription>
        </SheetHeader>
        <form onSubmit={handleSave}>
          {/* Form fields */}
        </form>
        <SheetFooter>
          <Button onClick={handleSave}>Save</Button>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}
```

### Without Close Button (Forced Choice)

```tsx
<Sheet>
  <SheetTrigger render={<Button variant="destructive" />}>
    Delete Account
  </SheetTrigger>
  <SheetContent showCloseButton={false}>
    <SheetHeader>
      <SheetTitle>Confirm Action</SheetTitle>
      <SheetDescription>
        This action requires confirmation
      </SheetDescription>
    </SheetHeader>
    <div className="py-6">
      <p className="text-sm">
        Are you sure? This cannot be undone.
      </p>
    </div>
    <SheetFooter>
      <SheetClose render={<Button variant="outline" />}>
        Cancel
      </SheetClose>
      <SheetClose render={<Button variant="destructive" />}>
        Confirm Delete
      </SheetClose>
    </SheetFooter>
  </SheetContent>
</Sheet>
```

## Keyboard

| Key | Action |
|-----|--------|
| Escape | Close sheet (unless `modal={false}`) |
| Tab | Navigate through focusable elements |

## Accessibility

- **Focus Management**: Focus moves to sheet content when opened, returns to trigger when closed
- **ARIA**: Title and description are automatically linked via `aria-labelledby` and `aria-describedby`
- **Keyboard**: Full keyboard navigation support with Escape to close
- **Screen Readers**: Announced as dialog/modal with proper labeling

## Related

- [Dialog](./dialog.llm.md) - For centered modal dialogs
- [Drawer](./drawer.llm.md) - Similar to Sheet but with dismissible overlay behavior
- [Popover](./popover.llm.md) - For smaller contextual overlays anchored to elements

---

# Sidebar

Comprehensive collapsible sidebar system with responsive behavior, keyboard shortcuts, and state persistence.

## Import

```tsx
import {
  Sidebar,
  SidebarProvider,
  SidebarTrigger,
  SidebarInset,
  SidebarContent,
  SidebarHeader,
  SidebarFooter,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuButton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarGroupContent,
  SidebarGroupAction,
  SidebarSeparator,
  SidebarInput,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuSkeleton,
  SidebarRail,
  useSidebar,
} from "@neynar/ui/sidebar"
```

## Anatomy

```tsx
<SidebarProvider>
  <Sidebar>
    <SidebarHeader>
      <SidebarMenu>
        <SidebarMenuItem>
          <SidebarMenuButton>Header Content</SidebarMenuButton>
        </SidebarMenuItem>
      </SidebarMenu>
    </SidebarHeader>

    <SidebarContent>
      <SidebarGroup>
        <SidebarGroupLabel>Section</SidebarGroupLabel>
        <SidebarGroupContent>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton>Item</SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarGroupContent>
      </SidebarGroup>
    </SidebarContent>

    <SidebarFooter>
      <SidebarMenu>
        <SidebarMenuItem>
          <SidebarMenuButton>Footer Content</SidebarMenuButton>
        </SidebarMenuItem>
      </SidebarMenu>
    </SidebarFooter>

    <SidebarRail />
  </Sidebar>

  <SidebarInset>
    <header>
      <SidebarTrigger />
      <h1>Page Title</h1>
    </header>
    <main>Main content</main>
  </SidebarInset>
</SidebarProvider>
```

## Components

| Component | Description |
|-----------|-------------|
| SidebarProvider | Root provider managing state, keyboard shortcuts, persistence |
| Sidebar | Main sidebar container with responsive behavior |
| SidebarTrigger | Toggle button (hamburger icon) |
| SidebarInset | Main content area that adjusts to sidebar state |
| SidebarContent | Scrollable content area containing groups |
| SidebarHeader | Top section for branding/app switcher |
| SidebarFooter | Bottom section for user profile/settings |
| SidebarMenu | Menu list container |
| SidebarMenuItem | Menu item wrapper |
| SidebarMenuButton | Clickable menu button with variants |
| SidebarMenuSub | Nested submenu list |
| SidebarMenuSubButton | Submenu item button |
| SidebarMenuSubItem | Submenu item wrapper |
| SidebarGroup | Section container with optional label |
| SidebarGroupLabel | Section heading |
| SidebarGroupContent | Section content wrapper |
| SidebarGroupAction | Action button in group header |
| SidebarSeparator | Horizontal divider |
| SidebarInput | Search/filter input |
| SidebarMenuAction | Secondary action on menu items |
| SidebarMenuBadge | Notification badge on menu items |
| SidebarMenuSkeleton | Loading skeleton |
| SidebarRail | Drag-to-resize rail element |
| useSidebar | Hook to access sidebar context |

## Props

### SidebarProvider

Root wrapper managing all sidebar state and behavior.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| defaultOpen | boolean | true | Initial open state on desktop |
| open | boolean | - | Controlled open state |
| onOpenChange | (open: boolean) => void | - | State change callback (persists to cookie) |

**Auto-behaviors:**
- Keyboard shortcut: Cmd/Ctrl+B toggles sidebar
- Mobile detection: Renders Sheet on mobile
- Cookie persistence: Saves state as `sidebar_state`
- 7-day cookie expiration

### Sidebar

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| side | "left" \| "right" | "left" | Which side to display |
| variant | "sidebar" \| "floating" \| "inset" | "sidebar" | Visual variant |
| collapsible | "offcanvas" \| "icon" \| "none" | "offcanvas" | Collapse behavior |

**Variants:**
- `sidebar`: Standard edge-to-edge sidebar
- `floating`: Sidebar with padding and rounded corners
- `inset`: Sidebar with inset content area

**Collapsible modes:**
- `offcanvas`: Slides completely off screen
- `icon`: Collapses to icon-only mode
- `none`: Always visible, non-collapsible

### SidebarMenuButton

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| isActive | boolean | false | Mark as active/current page |
| variant | "default" \| "outline" | "default" | Button variant |
| size | "default" \| "sm" \| "lg" | "default" | Button size |
| tooltip | string \| TooltipProps | - | Tooltip shown when collapsed |
| render | ReactElement | - | Custom element to render as |

### SidebarMenuAction

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| showOnHover | boolean | false | Only show on hover/focus |
| render | ReactElement | - | Custom element to render as |

### SidebarMenuSkeleton

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| showIcon | boolean | false | Show icon skeleton |

**Auto-behaviors:**
- Randomizes width between 50-90% for realistic loading state

### SidebarMenuSubButton

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| size | "sm" \| "md" | "md" | Button size |
| isActive | boolean | false | Mark as active/current page |
| render | ReactElement | - | Custom element to render as |

## Render Prop Pattern

Components with `render` prop support custom elements:

```tsx
// Render as link
<SidebarMenuButton render={<a href="/dashboard" />}>
  Dashboard
</SidebarMenuButton>

// Render as Next.js Link
<SidebarMenuButton render={<Link href="/settings" />}>
  Settings
</SidebarMenuButton>

// Render as custom component
<SidebarMenuSubButton render={<NavLink to="/docs" />}>
  Documentation
</SidebarMenuSubButton>
```

## Data Attributes

| Attribute | When Present | Applied To |
|-----------|--------------|------------|
| data-state | "expanded" \| "collapsed" | Sidebar container |
| data-collapsible | "offcanvas" \| "icon" | Sidebar when collapsed |
| data-variant | "sidebar" \| "floating" \| "inset" | Sidebar container |
| data-side | "left" \| "right" | Sidebar container |
| data-active | When isActive=true | SidebarMenuButton |
| data-open | When tooltip open | SidebarMenuButton |
| data-mobile | "true" | Mobile sidebar (Sheet) |

## Examples

### Basic Sidebar

```tsx
import { SidebarProvider, Sidebar, SidebarContent, SidebarInset, SidebarTrigger } from "@neynar/ui/sidebar"
import { HomeIcon } from "lucide-react"

function App() {
  return (
    <SidebarProvider>
      <Sidebar>
        <SidebarContent>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton isActive>
                <HomeIcon className="size-4" />
                <span>Home</span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarContent>
      </Sidebar>

      <SidebarInset>
        <header className="flex h-16 items-center gap-4 border-b px-6">
          <SidebarTrigger />
          <h1>Dashboard</h1>
        </header>
        <main className="p-6">Content</main>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

### With Header and Footer

```tsx
<Sidebar>
  <SidebarHeader>
    <SidebarMenu>
      <SidebarMenuItem>
        <SidebarMenuButton className="h-12">
          <div className="bg-primary/10 flex size-8 items-center justify-center rounded-lg">
            <LayoutDashboardIcon className="size-4" />
          </div>
          <div className="flex flex-col">
            <span className="font-semibold">Neynar</span>
            <span className="text-muted-foreground text-xs">Developer Portal</span>
          </div>
        </SidebarMenuButton>
      </SidebarMenuItem>
    </SidebarMenu>
  </SidebarHeader>

  <SidebarContent>
    {/* Menu groups */}
  </SidebarContent>

  <SidebarFooter>
    <SidebarMenu>
      <SidebarMenuItem>
        <SidebarMenuButton className="h-12">
          <Avatar className="size-8">
            <AvatarFallback>JD</AvatarFallback>
          </Avatar>
          <div className="flex flex-col">
            <span className="text-sm font-medium">John Developer</span>
            <span className="text-muted-foreground text-xs">john@example.com</span>
          </div>
        </SidebarMenuButton>
      </SidebarMenuItem>
    </SidebarMenu>
  </SidebarFooter>
</Sidebar>
```

### Grouped Navigation

```tsx
<SidebarContent>
  <SidebarGroup>
    <SidebarGroupLabel>Dashboard</SidebarGroupLabel>
    <SidebarGroupContent>
      <SidebarMenu>
        <SidebarMenuItem>
          <SidebarMenuButton isActive>
            <HomeIcon className="size-4" />
            <span>Overview</span>
          </SidebarMenuButton>
        </SidebarMenuItem>
        <SidebarMenuItem>
          <SidebarMenuButton>
            <BarChartIcon className="size-4" />
            <span>Analytics</span>
          </SidebarMenuButton>
        </SidebarMenuItem>
      </SidebarMenu>
    </SidebarGroupContent>
  </SidebarGroup>

  <SidebarSeparator />

  <SidebarGroup>
    <SidebarGroupLabel>Settings</SidebarGroupLabel>
    <SidebarGroupContent>
      <SidebarMenu>
        <SidebarMenuItem>
          <SidebarMenuButton>
            <SettingsIcon className="size-4" />
            <span>General</span>
          </SidebarMenuButton>
        </SidebarMenuItem>
      </SidebarMenu>
    </SidebarGroupContent>
  </SidebarGroup>
</SidebarContent>
```

### With Nested Submenus

```tsx
<SidebarMenu>
  <SidebarMenuItem>
    <SidebarMenuButton>
      <BookOpenIcon className="size-4" />
      <span>Documentation</span>
    </SidebarMenuButton>
    <SidebarMenuSub>
      <SidebarMenuSubItem>
        <SidebarMenuSubButton href="#intro">
          Introduction
        </SidebarMenuSubButton>
      </SidebarMenuSubItem>
      <SidebarMenuSubItem>
        <SidebarMenuSubButton href="#install" isActive>
          Installation
        </SidebarMenuSubButton>
      </SidebarMenuSubItem>
      <SidebarMenuSubItem>
        <SidebarMenuSubButton href="#quickstart">
          Quick Start
        </SidebarMenuSubButton>
      </SidebarMenuSubItem>
    </SidebarMenuSub>
  </SidebarMenuItem>
</SidebarMenu>
```

### With Badges and Actions

```tsx
<SidebarMenu>
  <SidebarMenuItem>
    <SidebarMenuButton>
      <MessageSquareIcon className="size-4" />
      <span>Messages</span>
      <Badge className="ml-auto">5</Badge>
    </SidebarMenuButton>
  </SidebarMenuItem>

  <SidebarMenuItem>
    <SidebarMenuButton>
      <KeyIcon className="size-4" />
      <span>API Keys</span>
    </SidebarMenuButton>
    <SidebarMenuAction showOnHover>
      <PlusIcon className="size-4" />
    </SidebarMenuAction>
  </SidebarMenuItem>
</SidebarMenu>
```

### Controlled State

```tsx
function App() {
  const [open, setOpen] = useState(true)

  return (
    <SidebarProvider open={open} onOpenChange={setOpen}>
      <Sidebar>
        {/* sidebar content */}
      </Sidebar>

      <SidebarInset>
        <button onClick={() => setOpen(!open)}>
          {open ? "Close" : "Open"} Sidebar
        </button>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

### With Collapsible Groups

```tsx
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@neynar/ui/collapsible"

<SidebarGroup>
  <Collapsible defaultOpen>
    <SidebarGroupLabel>
      <CollapsibleTrigger className="flex w-full items-center">
        <BookOpenIcon className="mr-2 size-4" />
        Documentation
        <ChevronDownIcon className="ml-auto size-4 transition-transform group-data-[state=open]:rotate-180" />
      </CollapsibleTrigger>
    </SidebarGroupLabel>
    <CollapsibleContent>
      <SidebarGroupContent>
        <SidebarMenuSub>
          {/* submenu items */}
        </SidebarMenuSub>
      </SidebarGroupContent>
    </CollapsibleContent>
  </Collapsible>
</SidebarGroup>
```

### Loading State

```tsx
<SidebarMenu>
  <SidebarMenuItem>
    <SidebarMenuSkeleton showIcon />
  </SidebarMenuItem>
  <SidebarMenuItem>
    <SidebarMenuSkeleton showIcon />
  </SidebarMenuItem>
  <SidebarMenuItem>
    <SidebarMenuSkeleton />
  </SidebarMenuItem>
</SidebarMenu>
```

### With Search Input

```tsx
<SidebarHeader>
  <SidebarInput
    type="search"
    placeholder="Search..."
    onChange={(e) => handleSearch(e.target.value)}
  />
</SidebarHeader>
```

### Using useSidebar Hook

```tsx
import { useSidebar } from "@neynar/ui/sidebar"

function CustomComponent() {
  const { state, open, toggleSidebar, isMobile } = useSidebar()

  return (
    <div>
      <p>Sidebar is {state}</p>
      <p>Mobile: {isMobile ? "Yes" : "No"}</p>
      <button onClick={toggleSidebar}>Toggle</button>
    </div>
  )
}
```

## Keyboard

| Key | Action |
|-----|--------|
| Cmd/Ctrl+B | Toggle sidebar open/closed |
| Escape | Close sidebar (mobile only) |
| Tab | Navigate between menu items |

## Accessibility

- Full keyboard navigation support with Tab/Shift+Tab
- Screen reader labels on SidebarTrigger and SidebarRail
- ARIA attributes managed automatically via data-attributes
- Focus management when toggling sidebar state
- Semantic HTML structure (nav, ul, li, button elements)
- Mobile sidebar uses Sheet with proper dialog semantics

## CSS Variables

| Variable | Description |
|----------|-------------|
| --sidebar-width | Desktop sidebar width (16rem) |
| --sidebar-width-mobile | Mobile sidebar width (18rem) |
| --sidebar-width-icon | Icon-only mode width (3rem) |

## Styling

Use data attributes for conditional styling:

```tsx
// Style based on sidebar state
className="hidden md:block group-data-[state=collapsed]:md:hidden"

// Style based on collapse mode
className="group-data-[collapsible=icon]:hidden"

// Style based on variant
className="group-data-[variant=floating]:rounded-lg"
```

## Related

- [Sheet](/components/sheet) - Used for mobile sidebar
- [Collapsible](/components/collapsible) - For collapsible groups
- [Button](/components/button) - SidebarMenuButton inherits from Button
- [Tooltip](/components/tooltip) - For collapsed state tooltips
- [Separator](/components/separator) - SidebarSeparator

---

# Skeleton

Loading placeholder with pulse animation to indicate content is being fetched.

## Import

```tsx
import { Skeleton } from "@neynar/ui/skeleton"
```

## Anatomy

```tsx
<Skeleton className="h-4 w-full" />
```

## Props

Accepts all standard HTML `div` attributes via `React.ComponentProps<"div">`:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional CSS classes for custom sizing and shape |

The Skeleton component applies default styling: `bg-muted rounded-md animate-pulse`. Use `className` to override dimensions and border radius.

## Data Attributes

| Attribute | When Present |
|-----------|--------------|
| data-slot="skeleton" | Always present for styling hooks |

## Examples

### Text Line

```tsx
<Skeleton className="h-4 w-full" />
```

### Multiple Lines

```tsx
<div className="space-y-2">
  <Skeleton className="h-4 w-full" />
  <Skeleton className="h-4 w-5/6" />
  <Skeleton className="h-4 w-4/6" />
</div>
```

### Avatar

```tsx
<Skeleton className="size-12 rounded-full" />
```

### User Profile Card

```tsx
<div className="flex items-center gap-4">
  <Skeleton className="size-16 rounded-full" />
  <div className="flex-1 space-y-2">
    <Skeleton className="h-5 w-32" />
    <Skeleton className="h-4 w-24" />
  </div>
</div>
```

### Button

```tsx
<Skeleton className="h-10 w-24" />
```

### Card with Image

```tsx
<div className="border rounded-lg p-4">
  <Skeleton className="aspect-video w-full mb-4" />
  <Skeleton className="h-5 w-3/4 mb-2" />
  <Skeleton className="h-4 w-full" />
  <Skeleton className="h-4 w-5/6 mt-2" />
</div>
```

### Cast Feed Item

```tsx
<div className="flex gap-3">
  <Skeleton className="size-10 rounded-full" />
  <div className="flex-1 space-y-3">
    <div className="flex items-center gap-2">
      <Skeleton className="h-4 w-24" />
      <Skeleton className="h-3 w-16" />
    </div>
    <div className="space-y-2">
      <Skeleton className="h-4 w-full" />
      <Skeleton className="h-4 w-5/6" />
    </div>
    <div className="flex gap-4">
      <Skeleton className="h-8 w-16" />
      <Skeleton className="h-8 w-16" />
    </div>
  </div>
</div>
```

### API Analytics Card

```tsx
<div className="border rounded-lg p-4">
  <Skeleton className="h-4 w-24 mb-2" />
  <Skeleton className="h-8 w-16 mb-4" />
  <Skeleton className="h-2 w-full" />
</div>
```

### Data Table Row

```tsx
<div className="flex items-center gap-4 p-4">
  <Skeleton className="size-8 rounded" />
  <Skeleton className="h-4 flex-1" />
  <Skeleton className="h-4 w-24" />
  <Skeleton className="h-4 w-32" />
  <Skeleton className="size-8 rounded" />
</div>
```

## Accessibility

- Skeleton is purely visual and doesn't convey semantic meaning
- Ensure parent containers have appropriate ARIA live regions when content updates
- Consider using `aria-busy="true"` on loading containers
- Maintain proper layout structure so screen readers understand context when content loads

## Related

- [Spinner](./spinner.llm.md) - For indeterminate loading states
- [Progress](./progress.llm.md) - For determinate loading states

---

# Slider

Slider for selecting single values or ranges with one or more draggable thumbs.

## Import

```tsx
import { Slider } from "@neynar/ui/slider"
```

## Anatomy

```tsx
<Slider defaultValue={[50]} min={0} max={100} />
```

## Props

All props from Base UI Slider.Root are supported.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| defaultValue | number \| number[] | - | Uncontrolled initial value. Use array for ranges. |
| value | number \| number[] | - | Controlled value. Use array for ranges. |
| onValueChange | (value: number \| number[], eventDetails) => void | - | Called when value changes during interaction. |
| onValueCommitted | (value: number \| number[], eventDetails) => void | - | Called when interaction completes (pointerup). |
| min | number | 0 | Minimum value. |
| max | number | 100 | Maximum value. |
| step | number | 1 | Step increment for value changes. |
| largeStep | number | 10 | Step for Page Up/Down or Shift + Arrow keys. |
| minStepsBetweenValues | number | - | Minimum steps between thumbs in range slider. |
| disabled | boolean | false | Disables all interactions. |
| orientation | "horizontal" \| "vertical" | "horizontal" | Slider direction. |
| thumbAlignment | "center" \| "edge" \| "edge-client-only" | "edge" | How thumbs align at min/max positions. |
| thumbCollisionBehavior | "none" \| "push" \| "swap" | "push" | How thumbs interact when they collide. |
| name | string | - | Form field name for submission. |
| className | string | - | Additional classes for the control element. |

### Value Format

- **Single value**: `defaultValue={[50]}` or `value={[50]}`
- **Range (two thumbs)**: `defaultValue={[25, 75]}`
- **Multiple thumbs**: `defaultValue={[20, 40, 60, 80]}`

Values are always arrays. Component automatically creates the correct number of thumbs based on array length.

### Thumb Alignment

Set to `"edge"` by default (thumbs align to edges at min/max):
- `"center"` - Thumb centers align at min/max
- `"edge"` - Thumb edges align at min/max (default)
- `"edge-client-only"` - Like edge but only on client

## Data Attributes

Applied to control element for styling:

| Attribute | When Present |
|-----------|--------------|
| data-disabled | Slider is disabled |
| data-horizontal | Orientation is horizontal |
| data-vertical | Orientation is vertical |

Applied to individual elements via `data-slot`:
- `data-slot="slider"` - Root element
- `data-slot="slider-track"` - Track element
- `data-slot="slider-range"` - Filled indicator
- `data-slot="slider-thumb"` - Draggable thumb(s)

## Examples

### Basic Single Value

```tsx
function VolumeControl() {
  return (
    <div className="space-y-2">
      <label>Volume</label>
      <Slider defaultValue={[50]} min={0} max={100} />
    </div>
  )
}
```

### Controlled with Display

```tsx
function RateLimitControl() {
  const [value, setValue] = useState([60])

  return (
    <div className="space-y-2">
      <div className="flex justify-between">
        <label>Requests per minute</label>
        <span className="font-mono">{value[0]}</span>
      </div>
      <Slider
        value={value}
        onValueChange={(v) => setValue(Array.isArray(v) ? [...v] : [v])}
        min={10}
        max={200}
        step={10}
      />
    </div>
  )
}
```

### Range Selection

```tsx
function PriceRangeFilter() {
  const [range, setRange] = useState([25, 75])

  return (
    <div className="space-y-2">
      <label>Price Range: ${range[0]} - ${range[1]}</label>
      <Slider
        value={range}
        onValueChange={(v) => setRange(Array.isArray(v) ? [...v] : [v])}
        min={0}
        max={100}
        step={5}
      />
    </div>
  )
}
```

### With Step Increments

```tsx
<Slider
  defaultValue={[50]}
  min={0}
  max={100}
  step={25}
/>
```

### Vertical Orientation

```tsx
<div className="h-48">
  <Slider
    defaultValue={[50]}
    min={0}
    max={100}
    orientation="vertical"
  />
</div>
```

### Multiple Thresholds

```tsx
function AlertThresholds() {
  const [thresholds, setThresholds] = useState([25, 50, 75])

  return (
    <div className="space-y-2">
      <label>Alert Levels: {thresholds.join(', ')}</label>
      <Slider
        value={thresholds}
        onValueChange={(v) => setThresholds(Array.isArray(v) ? [...v] : [v])}
        min={0}
        max={100}
        step={5}
      />
    </div>
  )
}
```

### Disabled State

```tsx
<Slider
  defaultValue={[50]}
  min={0}
  max={100}
  disabled
/>
```

## Keyboard Navigation

| Key | Action |
|-----|--------|
| Arrow Left/Down | Decrease value by `step` |
| Arrow Right/Up | Increase value by `step` |
| Page Down | Decrease by `largeStep` (default 10) |
| Page Up | Increase by `largeStep` (default 10) |
| Shift + Arrow | Increase/decrease by `largeStep` |
| Home | Set to minimum value |
| End | Set to maximum value |
| Tab | Focus next thumb |
| Shift + Tab | Focus previous thumb |

## Accessibility

- Uses ARIA `slider` role with proper attributes
- Announces current value, min, max, and orientation to screen readers
- Full keyboard navigation support
- Focus indicators on thumbs with hover/focus ring
- Disabled state prevents interaction and reduces opacity
- Touch-friendly with adequate thumb size (16px)

## Related

- [Input](./input.llm.md) - For precise numeric entry
- [Select](./select.llm.md) - For discrete value selection
- [RadioGroup](./radio-group.llm.md) - For choosing one option

---

# Sonner (Toast Notifications)

Toast notification system built on Sonner with themed icons and automatic color system integration.

## Import

```tsx
import { Toaster, toast } from "@neynar/ui/sonner"
```

## Anatomy

```tsx
// Add once to your app root
<Toaster position="bottom-right" />

// Trigger toasts anywhere in your app
toast.success("Saved successfully")
toast.error("Failed to save")
```

## Components

| Component | Description |
|-----------|-------------|
| Toaster | Container component rendered once at app root. Manages all toast notifications. |
| toast | Function API for triggering notifications. Includes type variants and promise handling. |

## Props

### Toaster

Pre-configured toast container with custom icons and theme integration. Renders at the specified position and manages all active toasts.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| position | "top-left" \| "top-center" \| "top-right" \| "bottom-left" \| "bottom-center" \| "bottom-right" | "bottom-right" | Screen position for toasts |
| theme | "light" \| "dark" \| "system" | "system" | Color theme for toasts |
| richColors | boolean | true | Enhanced colors for toast variants |
| expand | boolean | false | Expand toasts on hover |
| visibleToasts | number | 3 | Maximum visible toasts at once |
| closeButton | boolean | false | Show close button on all toasts |
| duration | number | 4000 | Default duration in milliseconds |
| toastOptions | object | - | Global toast styling options |

### toast Function API

Function for triggering toast notifications. Available variants:

```tsx
// Basic toast
toast("Message")

// Type variants
toast.success("Success message")
toast.error("Error message")
toast.warning("Warning message")
toast.info("Info message")
toast.loading("Loading...")

// Promise handling
toast.promise(promise, {
  loading: "Loading...",
  success: "Success!",
  error: "Failed"
})
```

### Toast Options

All toast functions accept an optional second parameter with these options:

| Option | Type | Description |
|--------|------|-------------|
| description | string | Secondary text below the title |
| duration | number | Override default duration (milliseconds) |
| icon | ReactNode | Custom icon (replaces default) |
| action | { label: string, onClick: () => void } | Primary action button |
| cancel | { label: string, onClick: () => void } | Secondary cancel button |
| id | string | Custom toast ID for updates |
| position | string | Override global position |
| dismissible | boolean | Allow manual dismissal (default: true) |
| onDismiss | () => void | Callback when dismissed |
| onAutoClose | () => void | Callback when auto-closed |

## Customizations

### Custom Icons

Default icons are automatically provided:
- Success: `CircleCheckIcon`
- Error: `OctagonXIcon`
- Warning: `TriangleAlertIcon`
- Info: `InfoIcon`
- Loading: `Loader2Icon` (animated)

Override with the `icon` option:

```tsx
toast.success("Copied!", {
  icon: <CopyIcon className="size-4" />
})
```

### Theme Integration

Toasts automatically integrate with your theme's CSS variables:
- Uses `--popover` colors for normal toasts
- Uses `--success`, `--destructive`, `--warning`, `--info` for variants
- Applies `--radius` for border radius
- Includes backdrop blur effect

### Styling

Customize appearance via `toastOptions`:

```tsx
<Toaster
  toastOptions={{
    classNames: {
      toast: "custom-toast-class",
      title: "custom-title-class",
      description: "custom-description-class"
    },
    style: {
      background: "var(--custom-bg)",
      border: "1px solid var(--custom-border)"
    }
  }}
/>
```

## Examples

### Basic Usage

```tsx
function SaveButton() {
  const handleSave = async () => {
    try {
      await saveData()
      toast.success("Data saved successfully")
    } catch (error) {
      toast.error("Failed to save data")
    }
  }

  return <Button onClick={handleSave}>Save</Button>
}
```

### With Description

```tsx
toast.error("Failed to save", {
  description: "Please check your internet connection and try again"
})
```

### With Action Button

```tsx
toast.success("File uploaded", {
  description: "Your file has been uploaded successfully",
  action: {
    label: "View",
    onClick: () => router.push("/files")
  }
})
```

### With Both Actions

```tsx
toast.success("Changes saved", {
  description: "Do you want to publish these changes now?",
  action: {
    label: "Publish",
    onClick: () => publishChanges()
  },
  cancel: {
    label: "Later",
    onClick: () => console.log("Cancelled")
  }
})
```

### Promise Handling

```tsx
function CreateButton() {
  const handleCreate = () => {
    const promise = createAPIKey()

    toast.promise(promise, {
      loading: "Creating API key...",
      success: "API key created successfully",
      error: "Failed to create API key"
    })
  }

  return <Button onClick={handleCreate}>Create Key</Button>
}
```

### Promise with Dynamic Messages

```tsx
toast.promise(fetchUser(), {
  loading: "Loading user...",
  success: (data) => ({
    message: `Welcome ${data.name}!`,
    description: data.email,
    duration: 5000
  }),
  error: (error) => ({
    message: "Failed to load user",
    description: error.message
  })
})
```

### Loading State Management

```tsx
function RegenerateButton() {
  const handleRegenerate = async () => {
    const toastId = toast.loading("Regenerating API key...", {
      description: "This may take a few seconds"
    })

    try {
      const newKey = await regenerateKey()
      toast.success("API key regenerated", {
        id: toastId, // Updates existing toast
        description: "Your old key has been invalidated"
      })
    } catch (error) {
      toast.error("Failed to regenerate", {
        id: toastId
      })
    }
  }

  return <Button onClick={handleRegenerate}>Regenerate</Button>
}
```

### Custom Duration

```tsx
// Short notification (2 seconds)
toast.success("Copied!", { duration: 2000 })

// Long notification (10 seconds)
toast.warning("Important update", { duration: 10000 })

// Persistent until dismissed
toast.info("Review required", { duration: Infinity })
```

### Multiple Positions

```tsx
// Override global position per toast
toast.success("Top notification", { position: "top-center" })
toast.error("Bottom notification", { position: "bottom-left" })
```

### Stack Multiple Toasts

```tsx
function showBatch() {
  toast.success("First notification")
  setTimeout(() => toast.info("Second notification"), 200)
  setTimeout(() => toast.warning("Third notification"), 400)
}
```

## Keyboard

| Key | Action |
|-----|--------|
| Escape | Dismiss all toasts |
| Swipe | Dismiss individual toast (touch devices) |

## Accessibility

- ARIA live regions announce toast messages to screen readers
- Toasts are automatically announced with appropriate politeness level based on type
- Keyboard accessible dismissal with Escape key
- Focus management preserves user context
- Action buttons are keyboard navigable

## Related

- [Alert](./alert.llm.md) - Static inline notifications
- [Alert Dialog](./alert-dialog.llm.md) - Modal confirmations
- [Dialog](./dialog.llm.md) - Modal dialogs

---

# Spinner

Animated loading spinner for indicating processing states and asynchronous operations.

## Import

```tsx
import { Spinner } from "@neynar/ui/spinner"
```

## Anatomy

```tsx
<Spinner />
```

## Props

Extends all standard SVG element props (`React.ComponentProps<"svg">`), including `className`, `aria-*`, and SVG attributes.

### Common Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Custom CSS classes. Default size is `size-4` (16px) |
| color | string | - | Icon color (inherits from text color by default) |
| size | number \| string | - | Icon size in pixels or as string |
| strokeWidth | number \| string | - | Stroke width for the icon |

### Built-in Attributes

- `role="status"` - Announces loading state to screen readers
- `aria-label="Loading"` - Provides accessible label
- `animate-spin` - Continuous rotation animation

## Sizing

Use Tailwind size utilities via `className`:

```tsx
// Extra small (12px)
<Spinner className="size-3" />

// Small/Default (16px)
<Spinner className="size-4" />

// Medium (24px)
<Spinner className="size-6" />

// Large (32px)
<Spinner className="size-8" />

// Extra large (48px)
<Spinner className="size-12" />
```

## Examples

### Basic Loading Indicator

```tsx
<div className="flex items-center justify-center">
  <Spinner className="size-8" />
</div>
```

### With Loading Text

```tsx
<div className="flex flex-col items-center gap-3">
  <Spinner className="size-8" />
  <p className="text-sm text-muted-foreground">Loading content...</p>
</div>
```

### In Button (Loading State)

```tsx
<Button disabled>
  <Spinner data-icon="inline-start" />
  Loading...
</Button>
```

The `data-icon="inline-start"` attribute uses Button's internal spacing for icons at the start position.

### Small Button

```tsx
<Button size="sm" disabled>
  <Spinner data-icon="inline-start" className="size-3.5" />
  Saving...
</Button>
```

### Inline with Text

```tsx
<div className="flex items-center gap-2">
  <Spinner className="size-4" />
  <span className="text-sm">Syncing with Farcaster network...</span>
</div>
```

### Custom Colors

```tsx
// Primary color
<Spinner className="size-6 text-primary" />

// Custom color
<Spinner className="size-6 text-blue-600" />

// Destructive
<Spinner className="size-6 text-destructive" />

// Muted
<Spinner className="size-6 text-muted-foreground" />
```

### Full-Page Loading

```tsx
<div className="flex min-h-[400px] items-center justify-center">
  <Spinner className="size-12" />
</div>
```

### Card Loading State

```tsx
<Card>
  <CardContent className="flex min-h-[200px] flex-col items-center justify-center gap-3">
    <Spinner className="size-8" />
    <p className="text-sm text-muted-foreground">Loading activity feed...</p>
  </CardContent>
</Card>
```

### Conditional Loading with Success State

```tsx
function DataLoader() {
  const [status, setStatus] = useState<"loading" | "success">("loading")

  return (
    <Button onClick={handleLoad} disabled={status === "loading"}>
      {status === "loading" ? (
        <Spinner data-icon="inline-start" />
      ) : (
        <CheckCircle2Icon data-icon="inline-start" className="text-green-600" />
      )}
      {status === "loading" ? "Loading..." : "Fetch Data"}
    </Button>
  )
}
```

## Size Reference

| Class | Pixels | Use Case |
|-------|--------|----------|
| `size-3` | 12px | Extra small buttons, tight spaces |
| `size-4` | 16px | Default, inline text, small buttons |
| `size-6` | 24px | Medium buttons, prominent indicators |
| `size-8` | 32px | Large buttons, card loading states |
| `size-12` | 48px | Full page loading, empty states |

## Accessibility

- Built-in `role="status"` for ARIA live region announcement
- `aria-label="Loading"` provides screen reader context
- Animation respects `prefers-reduced-motion` when configured
- Color contrast should meet WCAG AA standards (use appropriate text colors)

## Implementation Notes

- Uses Lucide's `Loader2Icon` component
- Animated via Tailwind's `animate-spin` utility
- Inherits text color by default for easy theming
- SVG renders inline for optimal performance
- No additional JavaScript runtime overhead

## Related

- [Button](./button.llm.md) - Often used with Spinner for loading states
- [Skeleton](./skeleton.llm.md) - Alternative loading indicator for content placeholders

---

# Switch

Toggle switch for binary on/off settings with keyboard support and form integration.

## Import

```tsx
import { Switch } from "@neynar/ui/switch"
```

## Anatomy

```tsx
<Switch />
```

The Switch component is a single component that internally uses `Switch.Root` and `Switch.Thumb` from Base UI.

## Props

### Switch

Extends all props from `Switch.Root.Props` from `@base-ui/react/switch`.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| size | "sm" \| "default" | "default" | Size variant of the switch |
| checked | boolean | - | Controlled checked state |
| defaultChecked | boolean | false | Initial checked state (uncontrolled) |
| onCheckedChange | (checked: boolean, eventDetails) => void | - | Called when checked state changes |
| disabled | boolean | false | Disables interaction |
| readOnly | boolean | false | Prevents state changes but allows focus |
| required | boolean | false | Marks as required for form validation |
| name | string | - | Form field name |
| id | string | - | Element id for label association |
| aria-invalid | boolean | - | Shows error styling for validation |
| className | string | - | Additional CSS classes |

## Sizes

| Size | Dimensions | Use Case |
|------|------------|----------|
| sm | 14px × 24px | Compact layouts, dense settings panels |
| default | 18.4px × 32px | Standard size for most use cases |

## Data Attributes

The following data attributes are automatically applied for styling:

| Attribute | When Present |
|-----------|--------------|
| data-checked | Switch is on |
| data-unchecked | Switch is off |
| data-disabled | Switch is disabled |
| data-size="sm" \| "default" | Current size variant |

## Examples

### Basic Usage

```tsx
import { Switch } from "@neynar/ui/switch"
import { Label } from "@neynar/ui/label"

function Example() {
  return (
    <div className="flex items-center gap-3">
      <Switch id="notifications" />
      <Label htmlFor="notifications">Enable notifications</Label>
    </div>
  )
}
```

### Controlled State

```tsx
import { useState } from "react"
import { Switch } from "@neynar/ui/switch"
import { Label } from "@neynar/ui/label"

function ControlledExample() {
  const [enabled, setEnabled] = useState(false)

  return (
    <div className="space-y-2">
      <div className="flex items-center gap-3">
        <Switch
          id="webhooks"
          checked={enabled}
          onCheckedChange={setEnabled}
        />
        <Label htmlFor="webhooks">Enable webhooks</Label>
      </div>
      <p className="text-sm text-muted-foreground">
        Status: {enabled ? "Enabled" : "Disabled"}
      </p>
    </div>
  )
}
```

### Settings Panel

```tsx
import { Switch } from "@neynar/ui/switch"
import { Label } from "@neynar/ui/label"

function SettingsPanel() {
  return (
    <div className="border-border divide-border divide-y rounded-lg border">
      <div className="flex items-center justify-between p-4">
        <div className="space-y-0.5">
          <Label htmlFor="email" className="text-sm font-medium">
            Email Notifications
          </Label>
          <p className="text-muted-foreground text-xs">
            Receive alerts about important updates
          </p>
        </div>
        <Switch id="email" size="sm" defaultChecked />
      </div>
      <div className="flex items-center justify-between p-4">
        <div className="space-y-0.5">
          <Label htmlFor="marketing" className="text-sm font-medium">
            Marketing Emails
          </Label>
          <p className="text-muted-foreground text-xs">
            Product updates and announcements
          </p>
        </div>
        <Switch id="marketing" size="sm" />
      </div>
    </div>
  )
}
```

### Form Integration

```tsx
import { Switch } from "@neynar/ui/switch"
import { Label } from "@neynar/ui/label"

function FormExample() {
  return (
    <form>
      <div className="space-y-4">
        <div className="flex items-center justify-between">
          <Label htmlFor="terms" className="text-sm">
            I agree to the terms and conditions
          </Label>
          <Switch id="terms" name="terms" required />
        </div>
        <button type="submit">Submit</button>
      </div>
    </form>
  )
}
```

### With Disabled State

```tsx
import { Switch } from "@neynar/ui/switch"
import { Label } from "@neynar/ui/label"

function DisabledExample() {
  const [parentEnabled, setParentEnabled] = useState(false)

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between">
        <Label htmlFor="main">Enable Feature</Label>
        <Switch
          id="main"
          checked={parentEnabled}
          onCheckedChange={setParentEnabled}
        />
      </div>
      <div className="flex items-center justify-between">
        <Label htmlFor="sub" className="text-muted-foreground">
          Sub-option (requires feature)
        </Label>
        <Switch
          id="sub"
          size="sm"
          disabled={!parentEnabled}
          defaultChecked
        />
      </div>
    </div>
  )
}
```

### Invalid State

```tsx
import { Switch } from "@neynar/ui/switch"
import { Label } from "@neynar/ui/label"

function ValidationExample() {
  const [agreed, setAgreed] = useState(false)
  const [submitted, setSubmitted] = useState(false)

  const isInvalid = submitted && !agreed

  return (
    <div className="space-y-2">
      <div className="flex items-center justify-between">
        <Label htmlFor="required">Accept terms (required)</Label>
        <Switch
          id="required"
          checked={agreed}
          onCheckedChange={setAgreed}
          aria-invalid={isInvalid}
        />
      </div>
      {isInvalid && (
        <p className="text-destructive text-sm">
          You must accept the terms to continue
        </p>
      )}
      <button onClick={() => setSubmitted(true)}>Submit</button>
    </div>
  )
}
```

## Keyboard Interaction

| Key | Action |
|-----|--------|
| Space | Toggle checked state |
| Enter | Toggle checked state |
| Tab | Move focus to/from switch |

## Accessibility

- Renders a native `<input type="checkbox">` for form integration and screen readers
- Supports `aria-invalid` for validation error states
- Automatically manages focus states with visible focus ring
- Works with `<Label>` via `htmlFor` prop for clickable labels
- Disabled switches are properly announced and non-interactive

## Styling Notes

- Use `aria-invalid` prop to show error state (red border and ring)
- The switch has an extended click area (`after:absolute after:-inset-x-3 after:-inset-y-2`) for easier interaction
- Dark mode automatically adjusts colors for unchecked state background
- Focus ring uses `ring-ring/50` with 3px width for accessibility

## Related

- [Label](./label.llm.md) - Associate labels with switches
- [Checkbox](./checkbox.llm.md) - Alternative for yes/no choices
- [RadioGroup](./radio-group.llm.md) - Multiple exclusive options

---

# Table

Compound component for displaying tabular data with semantic HTML table structure and automatic responsive scrolling.

## Import

```tsx
import {
  Table,
  TableHeader,
  TableBody,
  TableFooter,
  TableHead,
  TableRow,
  TableCell,
  TableCaption,
} from "@neynar/ui/table"
```

## Anatomy

```tsx
<Table>
  <TableCaption>Optional description</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead>Column 1</TableHead>
      <TableHead>Column 2</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>Data 1</TableCell>
      <TableCell>Data 2</TableCell>
    </TableRow>
  </TableBody>
  <TableFooter>
    <TableRow>
      <TableCell colSpan={2}>Summary</TableCell>
    </TableRow>
  </TableFooter>
</Table>
```

## Components

| Component | Description |
|-----------|-------------|
| Table | Root container with automatic horizontal scroll |
| TableHeader | Header section containing column titles |
| TableBody | Body section containing data rows |
| TableFooter | Footer section for summary rows (totals, etc.) |
| TableRow | Individual row within header, body, or footer |
| TableHead | Header cell for column titles |
| TableCell | Data cell for row content |
| TableCaption | Accessible description rendered below table |

## Props

All components extend standard HTML table element props (`React.ComponentProps<"table">`, `React.ComponentProps<"thead">`, etc.).

### Table

Automatically wraps content in a responsive scroll container.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional classes for the table element |
| ...props | ComponentProps<"table"> | - | Standard table element props |

### TableHeader

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional classes for thead element |
| ...props | ComponentProps<"thead"> | - | Standard thead element props |

### TableBody

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional classes for tbody element |
| ...props | ComponentProps<"tbody"> | - | Standard tbody element props |

### TableFooter

Styled with muted background for visual distinction.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional classes for tfoot element |
| ...props | ComponentProps<"tfoot"> | - | Standard tfoot element props |

### TableRow

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| data-state | "selected" | - | Apply selected styling to row |
| className | string | - | Additional classes for tr element |
| ...props | ComponentProps<"tr"> | - | Standard tr element props |

### TableHead

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional classes for th element |
| ...props | ComponentProps<"th"> | - | Standard th element props (scope, etc.) |

### TableCell

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| colSpan | number | - | Number of columns to span |
| className | string | - | Additional classes for td element |
| ...props | ComponentProps<"td"> | - | Standard td element props |

### TableCaption

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string | - | Additional classes for caption element |
| ...props | ComponentProps<"caption"> | - | Standard caption element props |

## Data Attributes

| Attribute | Element | When Present |
|-----------|---------|--------------|
| data-state="selected" | TableRow | Row is visually highlighted as selected |
| data-slot | All | Identifies component part for styling ("table", "table-header", etc.) |

## Examples

### Basic Table

```tsx
<Table>
  <TableHeader>
    <TableRow>
      <TableHead>Invoice</TableHead>
      <TableHead>Status</TableHead>
      <TableHead className="text-right">Amount</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell className="font-medium">INV001</TableCell>
      <TableCell>Paid</TableCell>
      <TableCell className="text-right">$250.00</TableCell>
    </TableRow>
    <TableRow>
      <TableCell className="font-medium">INV002</TableCell>
      <TableCell>Pending</TableCell>
      <TableCell className="text-right">$150.00</TableCell>
    </TableRow>
  </TableBody>
  <TableFooter>
    <TableRow>
      <TableCell colSpan={2}>Total</TableCell>
      <TableCell className="text-right">$400.00</TableCell>
    </TableRow>
  </TableFooter>
</Table>
```

### With Status Badges

```tsx
import { Badge } from "@neynar/ui/badge"

<Table>
  <TableHeader>
    <TableRow>
      <TableHead>Service</TableHead>
      <TableHead>Status</TableHead>
      <TableHead>Uptime</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell className="font-medium">API Gateway</TableCell>
      <TableCell>
        <Badge variant="secondary">
          <CheckCircle2Icon className="size-3" />
          Operational
        </Badge>
      </TableCell>
      <TableCell>99.99%</TableCell>
    </TableRow>
    <TableRow>
      <TableCell className="font-medium">Webhooks</TableCell>
      <TableCell>
        <Badge variant="destructive">
          <XCircleIcon className="size-3" />
          Offline
        </Badge>
      </TableCell>
      <TableCell>85.20%</TableCell>
    </TableRow>
  </TableBody>
</Table>
```

### With Action Buttons

```tsx
import { Button } from "@neynar/ui/button"

<Table>
  <TableHeader>
    <TableRow>
      <TableHead>API Key</TableHead>
      <TableHead>Environment</TableHead>
      <TableHead className="text-right">Actions</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell className="font-mono text-xs">neynar_sk_***_8f3d</TableCell>
      <TableCell>
        <Badge variant="default">Production</Badge>
      </TableCell>
      <TableCell className="text-right">
        <div className="flex justify-end gap-2">
          <Button variant="ghost" size="icon-sm">
            <ExternalLinkIcon className="size-4" />
          </Button>
          <Button variant="ghost" size="icon-sm">
            <MoreHorizontalIcon className="size-4" />
          </Button>
        </div>
      </TableCell>
    </TableRow>
  </TableBody>
</Table>
```

### Selectable Rows

```tsx
<Table>
  <TableHeader>
    <TableRow>
      <TableHead>Name</TableHead>
      <TableHead>Role</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell className="font-medium">Alice Johnson</TableCell>
      <TableCell>Admin</TableCell>
    </TableRow>
    <TableRow data-state="selected">
      <TableCell className="font-medium">Bob Smith</TableCell>
      <TableCell>Developer</TableCell>
    </TableRow>
    <TableRow>
      <TableCell className="font-medium">Carol White</TableCell>
      <TableCell>Designer</TableCell>
    </TableRow>
  </TableBody>
</Table>
```

### With Caption

```tsx
<Table>
  <TableCaption>A list of your recent transactions</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead>Transaction</TableHead>
      <TableHead>Date</TableHead>
      <TableHead className="text-right">Amount</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell className="font-medium">Purchase</TableCell>
      <TableCell>2025-12-29</TableCell>
      <TableCell className="text-right">$99.00</TableCell>
    </TableRow>
  </TableBody>
</Table>
```

## Styling

### Responsive Scrolling

The Table component automatically provides horizontal scrolling for tables that exceed viewport width. No additional configuration needed.

### Compact Tables

Reduce spacing for dense data:

```tsx
<Table className="text-xs">
  <TableHeader>
    <TableRow>
      <TableHead className="h-8 px-2">ID</TableHead>
      <TableHead className="h-8 px-2">Event</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell className="p-1 px-2 font-mono">001</TableCell>
      <TableCell className="p-1 px-2">cast.created</TableCell>
    </TableRow>
  </TableBody>
</Table>
```

### Alignment

Use Tailwind utility classes for text alignment:

```tsx
<TableHead className="text-right">Amount</TableHead>
<TableCell className="text-center">Status</TableCell>
```

## Accessibility

- Semantic HTML table structure (`<table>`, `<thead>`, `<tbody>`, `<tfoot>`)
- TableCaption provides accessible description for screen readers
- TableHead uses `<th>` elements with implicit scope
- Proper heading hierarchy maintained
- All standard table ARIA attributes supported via props

## Related

- [Badge](/components/badge) - Status indicators within table cells
- [Button](/components/button) - Action buttons for row operations
- [Checkbox](/components/checkbox) - Selectable rows

---

# Tabs

Organize content into multiple panels with tab navigation, supporting both horizontal and vertical layouts.

## Import

```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@neynar/ui/tabs"
```

## Anatomy

```tsx
<Tabs defaultValue="tab1">
  <TabsList>
    <TabsTrigger value="tab1">Tab 1</TabsTrigger>
    <TabsTrigger value="tab2">Tab 2</TabsTrigger>
  </TabsList>
  <TabsContent value="tab1">Content 1</TabsContent>
  <TabsContent value="tab2">Content 2</TabsContent>
</Tabs>
```

## Components

| Component | Description |
|-----------|-------------|
| Tabs | Root container, manages active tab state and orientation |
| TabsList | Groups tab triggers, controls visual variant |
| TabsTrigger | Individual tab button with value identifier |
| TabsContent | Content panel shown when corresponding tab is active |

## Props

### Tabs

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| defaultValue | string \| number \| null | `0` | Initial active tab (uncontrolled) |
| value | string \| number \| null | - | Controlled active tab value |
| onValueChange | (value, eventDetails) => void | - | Called when active tab changes |
| orientation | "horizontal" \| "vertical" | "horizontal" | Layout direction |

When `value` is `null`, no tab will be active.

### TabsList

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "default" \| "line" | "default" | Visual style of tab list |
| activateOnFocus | boolean | - | Auto-activate tab on arrow key focus |
| loopFocus | boolean | - | Loop keyboard focus at list ends |

**Variants:**
- `default` - Tabs with muted background and active highlight
- `line` - Minimal tabs with underline indicator on active tab

### TabsTrigger

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string \| number | - | Identifier matching TabsContent value |
| disabled | boolean | - | Prevents tab activation |

Supports nested icons and badges as children.

### TabsContent

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string \| number | - | Identifier matching TabsTrigger value |
| keepMounted | boolean | false | Keep DOM element when hidden |

## Data Attributes

### Tabs, TabsList

| Attribute | When Present |
|-----------|--------------|
| data-orientation | "horizontal" or "vertical" |
| data-activation-direction | "left", "right", "up", "down", or "none" |

### TabsTrigger

| Attribute | When Present |
|-----------|--------------|
| data-active | Tab is currently active |
| data-disabled | Tab is disabled |

### TabsContent

| Attribute | When Present |
|-----------|--------------|
| data-hidden | Panel is not active |
| data-index | Panel index number |

## Examples

### Basic Usage

```tsx
<Tabs defaultValue="account">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
  <TabsContent value="account">
    Manage your account settings.
  </TabsContent>
  <TabsContent value="password">
    Change your password here.
  </TabsContent>
</Tabs>
```

### Controlled State

```tsx
function ControlledTabs() {
  const [activeTab, setActiveTab] = useState("webhooks")

  return (
    <Tabs value={activeTab} onValueChange={setActiveTab}>
      <TabsList>
        <TabsTrigger value="webhooks">Webhooks</TabsTrigger>
        <TabsTrigger value="keys">API Keys</TabsTrigger>
        <TabsTrigger value="logs">Logs</TabsTrigger>
      </TabsList>
      <TabsContent value="webhooks">...</TabsContent>
      <TabsContent value="keys">...</TabsContent>
      <TabsContent value="logs">...</TabsContent>
    </Tabs>
  )
}
```

### With Icons and Badges

```tsx
<Tabs defaultValue="inbox">
  <TabsList variant="line">
    <TabsTrigger value="inbox">
      <MailIcon />
      Inbox
      <Badge variant="default" className="ml-1.5">12</Badge>
    </TabsTrigger>
    <TabsTrigger value="drafts">
      <FileTextIcon />
      Drafts
      <Badge variant="secondary" className="ml-1.5">3</Badge>
    </TabsTrigger>
    <TabsTrigger value="sent">
      <SendIcon />
      Sent
    </TabsTrigger>
  </TabsList>
  <TabsContent value="inbox">12 unread messages</TabsContent>
  <TabsContent value="drafts">3 draft messages</TabsContent>
  <TabsContent value="sent">Sent messages</TabsContent>
</Tabs>
```

### Line Variant

```tsx
<Tabs defaultValue="all">
  <TabsList variant="line">
    <TabsTrigger value="all">All</TabsTrigger>
    <TabsTrigger value="active">Active</TabsTrigger>
    <TabsTrigger value="completed">Completed</TabsTrigger>
  </TabsList>
  <TabsContent value="all">All items</TabsContent>
  <TabsContent value="active">Active items</TabsContent>
  <TabsContent value="completed">Completed items</TabsContent>
</Tabs>
```

### Vertical Orientation

```tsx
<Tabs defaultValue="profile" orientation="vertical" className="w-[600px]">
  <TabsList>
    <TabsTrigger value="profile">
      <UserIcon />
      Profile
    </TabsTrigger>
    <TabsTrigger value="account">
      <KeyIcon />
      Account
    </TabsTrigger>
    <TabsTrigger value="notifications">
      <BellIcon />
      Notifications
    </TabsTrigger>
  </TabsList>
  <TabsContent value="profile">Profile settings</TabsContent>
  <TabsContent value="account">Account security</TabsContent>
  <TabsContent value="notifications">Notification preferences</TabsContent>
</Tabs>
```

### Disabled Tabs

```tsx
<Tabs defaultValue="general">
  <TabsList>
    <TabsTrigger value="general">General</TabsTrigger>
    <TabsTrigger value="privacy">Privacy</TabsTrigger>
    <TabsTrigger value="billing" disabled>
      Billing
    </TabsTrigger>
    <TabsTrigger value="team" disabled>
      Team
      <Badge variant="outline" className="ml-1.5">Pro</Badge>
    </TabsTrigger>
  </TabsList>
  <TabsContent value="general">General settings</TabsContent>
  <TabsContent value="privacy">Privacy settings</TabsContent>
</Tabs>
```

## Keyboard

| Key | Action |
|-----|--------|
| Tab | Move focus to next focusable element |
| ArrowLeft / ArrowRight | Navigate between tabs (horizontal) |
| ArrowUp / ArrowDown | Navigate between tabs (vertical) |
| Enter / Space | Activate focused tab (when activateOnFocus is false) |
| Home | Focus first tab |
| End | Focus last tab |

## Accessibility

- Uses `role="tablist"`, `role="tab"`, and `role="tabpanel"` for proper semantic structure
- Active tab marked with `aria-selected="true"`
- Tab panels linked via `aria-labelledby` and `aria-controls`
- Keyboard navigation follows ARIA Authoring Practices Guide
- Focus management automatically handled when switching tabs

## Related

- [Button](./button.llm.md) - For tab-like actions
- [Badge](./badge.llm.md) - For counts in tabs
- [Card](./card.llm.md) - For content panels

---

# Text

Paragraph text component with size, weight, color, alignment, and truncation options.

## Import

```tsx
import { Text, Caption, Overline } from "@neynar/ui/typography"
```

## Usage

```tsx
<Text>Default paragraph text</Text>
<Text size="lg" weight="semibold">Large semibold text</Text>
<Text color="muted">Secondary text</Text>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| size | "xs" \| "sm" \| "base" \| "lg" \| "xl" \| "2xl" | "base" | Text size |
| weight | "normal" \| "medium" \| "semibold" \| "bold" | - | Font weight |
| color | "default" \| "muted" \| "subtle" \| "destructive" \| "success" \| "warning" \| "info" | "default" | Text color |
| align | "left" \| "center" \| "right" | - | Text alignment |
| transform | "uppercase" \| "lowercase" \| "capitalize" | - | Text transform |
| wrap | "pretty" \| "balance" | - | Text wrapping strategy |
| lineClamp | 1 \| 2 \| 3 \| 4 \| 5 \| 6 | - | Truncate after N lines |
| as | React.ElementType | "p" | Change rendered element |
| render | React.ReactElement | - | Advanced polymorphism |

## Convenience Components

### Caption

Small muted text for metadata, timestamps, secondary info.

```tsx
<Caption>Posted 3 hours ago</Caption>
// Equivalent to: <Text size="xs" color="muted">
```

### Overline

Uppercase label text for categories and section headers.

```tsx
<Overline>Featured Article</Overline>
// Equivalent to: <Text size="xs" transform="uppercase">
```

## Examples

### Sizes

```tsx
<Text size="xs">Extra small</Text>
<Text size="sm">Small</Text>
<Text size="base">Base (default)</Text>
<Text size="lg">Large</Text>
<Text size="xl">Extra large</Text>
```

### Colors

```tsx
<Text color="default">Default text</Text>
<Text color="muted">Muted helper text</Text>
<Text color="destructive">Error message</Text>
<Text color="success">Success message</Text>
```

### Truncation

```tsx
<Text lineClamp={2}>
  This long text will be truncated after two lines with an ellipsis...
</Text>
```

### Text Wrapping

```tsx
<Text wrap="balance">
  Balanced text distributes words evenly across lines
</Text>
<Text wrap="pretty">
  Pretty text avoids orphaned words on the last line
</Text>
```

### Polymorphism

```tsx
// Simple: change element
<Text as="span">Inline text</Text>
<Text as="label">Label text</Text>

// Advanced: render prop
<Text render={<a href="/link" />}>Link text</Text>
```

## Related

- [Title](./title.llm.md) - Heading component
- [Code](./code.llm.md) - Inline code
- [Blockquote](./blockquote.llm.md) - Quoted content

---

# Textarea

Multi-line text input with automatic vertical growth and comprehensive validation states.

## Import

```tsx
import { Textarea } from "@neynar/ui/textarea"
```

## Anatomy

```tsx
<Textarea placeholder="Enter text..." />
```

## Props

Extends all native `<textarea>` HTML props including:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| rows | number | - | Initial number of visible text rows |
| cols | number | - | Width in average character widths |
| disabled | boolean | false | Disables the textarea |
| aria-invalid | boolean | false | Shows error state with red border and ring |
| placeholder | string | - | Placeholder text |
| value | string | - | Controlled value |
| onChange | (e: ChangeEvent) => void | - | Change handler |
| className | string | - | Additional CSS classes |

All standard textarea attributes are supported (maxLength, autoFocus, readOnly, etc.)

## Auto-Grow Behavior

Textarea uses `field-sizing-content` to automatically grow vertically based on content:

```tsx
// Auto-grows from min-h-16 as content expands
<Textarea
  placeholder="Start typing and watch this grow..."
  className="min-h-16"
/>
```

The component has a default `min-h-16` (64px minimum height).

## Resize Control

Override the default resize behavior via className:

```tsx
// Disable resize handles
<Textarea className="resize-none" />

// Vertical resize only (default browser behavior)
<Textarea className="resize-y" />

// Both directions
<Textarea className="resize" />
```

## States

### Error State

Use `aria-invalid` for validation errors:

```tsx
<Textarea
  aria-invalid={hasError}
  placeholder="Required field..."
/>
```

Error styling:
- Red border (`border-destructive`)
- Red focus ring (`ring-destructive/20`, darker in dark mode)
- Works with Label component error state

### Disabled State

```tsx
<Textarea
  disabled
  value="Cannot edit this content"
/>
```

Disabled styling:
- 50% opacity
- No pointer cursor
- Not focusable

## Data Attributes

| Attribute | Purpose |
|-----------|---------|
| data-slot="textarea" | Component identifier for styling and testing |

## Examples

### Basic with Label

```tsx
import { Label } from "@neynar/ui/label"

<div className="space-y-2">
  <Label htmlFor="description">Description</Label>
  <Textarea
    id="description"
    placeholder="Enter a description..."
    rows={3}
  />
</div>
```

### Controlled with Character Count

```tsx
function CommentForm() {
  const [value, setValue] = useState("")
  const maxLength = 280
  const remaining = maxLength - value.length
  const isOverLimit = remaining < 0

  return (
    <div className="space-y-2">
      <Textarea
        value={value}
        onChange={(e) => setValue(e.target.value)}
        aria-invalid={isOverLimit}
        placeholder="What's on your mind?"
      />
      <p className={isOverLimit ? "text-destructive" : "text-muted-foreground"}>
        {remaining} characters remaining
      </p>
    </div>
  )
}
```

### Code/JSON Input

```tsx
<Textarea
  className="font-mono text-sm"
  placeholder="Paste JSON here..."
  rows={8}
  defaultValue={`{\n  "event": "cast.created",\n  "data": { ... }\n}`}
/>
```

### With Validation Error

```tsx
function FeedbackForm() {
  const [feedback, setFeedback] = useState("")
  const [error, setError] = useState("")

  const validate = () => {
    if (feedback.length < 10) {
      setError("Feedback must be at least 10 characters")
    }
  }

  return (
    <div className="space-y-2">
      <Label htmlFor="feedback" className={error ? "text-destructive" : ""}>
        Feedback
      </Label>
      <Textarea
        id="feedback"
        value={feedback}
        onChange={(e) => setFeedback(e.target.value)}
        onBlur={validate}
        aria-invalid={!!error}
      />
      {error && (
        <p className="text-destructive text-sm">{error}</p>
      )}
    </div>
  )
}
```

### Webhook Payload Editor

```tsx
<div className="space-y-2">
  <Label>Request Body (JSON)</Label>
  <Textarea
    className="font-mono text-sm"
    rows={8}
    defaultValue={webhookPayload}
  />
  <Button size="sm">
    <SendIcon data-icon="inline-start" />
    Send Test Request
  </Button>
</div>
```

## Keyboard

Standard textarea keyboard behavior:

| Key | Action |
|-----|--------|
| Tab | Move focus (can be prevented) |
| Enter | New line |
| Ctrl/Cmd + A | Select all |
| Ctrl/Cmd + Z | Undo |

## Accessibility

- Semantic `<textarea>` element
- Works with Label component via htmlFor/id
- Error state communicated via `aria-invalid`
- Placeholder provides input hint (not a replacement for label)
- Disabled state prevents focus and interaction
- Full keyboard navigation support

## Styling Notes

The component includes comprehensive focus and error states:
- **Focus**: Blue ring (`ring-ring/50`) with 3px width
- **Error**: Red border and ring in both light/dark modes
- **Dark mode**: Subtle background tint (`bg-input/30`)
- **Transitions**: Smooth color and shadow changes

## Related

- **[Label](/components/label.llm.md)** - Accessible form labels
- **[Input](/components/input.llm.md)** - Single-line text input
- **[Form](/components/form.llm.md)** - Form validation patterns

---

# Title

Semantic heading component (h1-h6) with independent visual sizing.

## Import

```tsx
import { Title } from "@neynar/ui/typography"
```

## Usage

```tsx
<Title>Default h2 heading</Title>
<Title order={1}>Page title (h1)</Title>
<Title order={3} size="xl">Visually large h3</Title>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| order | 1 \| 2 \| 3 \| 4 \| 5 \| 6 | 2 | Semantic heading level (h1-h6) |
| size | "xs" \| "sm" \| "base" \| "lg" \| "xl" \| "2xl" \| "3xl" \| "4xl" \| "5xl" \| "6xl" | auto | Visual size (overrides order default) |
| weight | "normal" \| "medium" \| "semibold" \| "bold" | auto | Font weight (overrides order default) |
| color | "default" \| "muted" \| "subtle" \| "destructive" \| "success" \| "warning" \| "info" | "default" | Text color |
| as | React.ElementType | auto | Change rendered element |
| render | React.ReactElement | - | Advanced polymorphism |

## Order Defaults

Each order has default size and weight:

| Order | Element | Default Size | Default Weight |
|-------|---------|--------------|----------------|
| 1 | h1 | 4xl | bold |
| 2 | h2 | 3xl | bold |
| 3 | h3 | 2xl | semibold |
| 4 | h4 | xl | semibold |
| 5 | h5 | lg | medium |
| 6 | h6 | base | medium |

## Examples

### Semantic Headings

```tsx
<Title order={1}>Page Title</Title>
<Title order={2}>Section Heading</Title>
<Title order={3}>Subsection</Title>
```

### Visual Override

```tsx
// Semantic h3, but visually as large as h1
<Title order={3} size="4xl">
  SEO-friendly h3 with hero styling
</Title>
```

### Colors

```tsx
<Title color="muted">Secondary heading</Title>
<Title color="destructive">Error heading</Title>
```

### Polymorphism

```tsx
// Render as different element
<Title as="div">Non-semantic title styling</Title>

// Advanced: render prop
<Title render={<a href="/section" />}>Linked heading</Title>
```

## Accessibility

- Use `order` for semantic structure (screen readers)
- Use `size` for visual presentation
- Don't skip heading levels (h1 → h3)

## Related

- [Text](./text.llm.md) - Paragraph text
- [Code](./code.llm.md) - Inline code

---

# ToggleGroup

A group of toggle buttons that share state, commonly used for formatting toolbars, view switchers, and filter controls.

## Import

```tsx
import { ToggleGroup, ToggleGroupItem } from "@neynar/ui/toggle-group"
```

## Anatomy

```tsx
<ToggleGroup>
  <ToggleGroupItem value="option1">Option 1</ToggleGroupItem>
  <ToggleGroupItem value="option2">Option 2</ToggleGroupItem>
  <ToggleGroupItem value="option3">Option 3</ToggleGroupItem>
</ToggleGroup>
```

## Components

| Component | Description |
|-----------|-------------|
| ToggleGroup | Root container that manages shared state and layout |
| ToggleGroupItem | Individual toggle button within the group |

## Props

### ToggleGroup

Inherits all props from `@base-ui/react/toggle-group` plus variant styling props.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | `any[]` | - | Controlled state: array of pressed item values |
| defaultValue | `any[]` | - | Uncontrolled initial state |
| onValueChange | `(value: any[]) => void` | - | Called when selection changes |
| multiple | `boolean` | `true` | Allow multiple items to be pressed |
| variant | `"default" \| "outline"` | - | Visual style inherited by all items |
| size | `"default" \| "sm" \| "lg"` | - | Size variant inherited by all items |
| spacing | `number` | `0` | Gap between items (0 = joined buttons) |
| orientation | `"horizontal" \| "vertical"` | `"horizontal"` | Layout direction |
| disabled | `boolean` | `false` | Disable all toggles in the group |
| loopFocus | `boolean` | `true` | Wrap focus when reaching end with arrow keys |

**Note:** By default, `multiple={true}` allows multiple selections. Set `multiple={false}` for single selection mode.

### ToggleGroupItem

Inherits variant and size from parent ToggleGroup context unless explicitly overridden.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | `string` | - | **Required.** Unique identifier for this item |
| variant | `"default" \| "outline"` | Inherits from group | Override group variant |
| size | `"default" \| "sm" \| "lg"` | Inherits from group | Override group size |
| aria-label | `string` | - | Required for icon-only buttons |

## Data Attributes

### ToggleGroup

| Attribute | When Present |
|-----------|--------------|
| data-orientation | Always: `"horizontal"` or `"vertical"` |
| data-disabled | Group is disabled |
| data-multiple | Multiple selection is enabled |
| data-spacing | Always: spacing value |
| data-variant | Always: variant name |
| data-size | Always: size name |

### ToggleGroupItem

| Attribute | When Present |
|-----------|--------------|
| data-pressed | Item is in pressed state |
| data-disabled | Item is disabled |

## Variants

### Visual Variants

| Variant | Appearance |
|---------|------------|
| default | Transparent background, fills on press |
| outline | Border with shadow, subtle background on press |

### Sizes

| Size | Height | Min Width | Padding |
|------|--------|-----------|---------|
| sm | 32px | 32px | 6px |
| default | 36px | 36px | 8px |
| lg | 40px | 40px | 10px |

## Examples

### Single Selection (Time Range Picker)

```tsx
function TimeRangePicker() {
  const [range, setRange] = useState(["7d"])

  return (
    <ToggleGroup
      value={range}
      onValueChange={(value) => {
        if (value) setRange(value)
      }}
      variant="outline"
      spacing={0}
    >
      <ToggleGroupItem value="1d">1D</ToggleGroupItem>
      <ToggleGroupItem value="7d">7D</ToggleGroupItem>
      <ToggleGroupItem value="30d">30D</ToggleGroupItem>
      <ToggleGroupItem value="90d">90D</ToggleGroupItem>
    </ToggleGroup>
  )
}
```

### Multiple Selection (Text Formatting)

```tsx
function TextFormatting() {
  return (
    <ToggleGroup variant="outline" spacing={0}>
      <ToggleGroupItem value="bold" aria-label="Bold">
        <BoldIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="italic" aria-label="Italic">
        <ItalicIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="underline" aria-label="Underline">
        <UnderlineIcon />
      </ToggleGroupItem>
    </ToggleGroup>
  )
}
```

### View Switcher (Icon-Only)

```tsx
function ViewSwitcher() {
  const [view, setView] = useState(["grid"])

  return (
    <ToggleGroup
      value={view}
      onValueChange={(value) => {
        if (value) setView(value)
      }}
      variant="outline"
      spacing={0}
    >
      <ToggleGroupItem value="grid" aria-label="Grid view">
        <GridIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="list" aria-label="List view">
        <ListIcon />
      </ToggleGroupItem>
    </ToggleGroup>
  )
}
```

### Vertical Layout with Spacing

```tsx
<ToggleGroup orientation="vertical" spacing={2} variant="outline">
  <ToggleGroupItem value="option1">Option 1</ToggleGroupItem>
  <ToggleGroupItem value="option2">Option 2</ToggleGroupItem>
  <ToggleGroupItem value="option3">Option 3</ToggleGroupItem>
</ToggleGroup>
```

### Separated Buttons (Spacing > 0)

```tsx
<ToggleGroup variant="outline" spacing={4}>
  <ToggleGroupItem value="filter1">Active</ToggleGroupItem>
  <ToggleGroupItem value="filter2">Pending</ToggleGroupItem>
  <ToggleGroupItem value="filter3">Archived</ToggleGroupItem>
</ToggleGroup>
```

## Keyboard Navigation

| Key | Action |
|-----|--------|
| Tab | Focus the group (first/last item based on direction) |
| ArrowRight / ArrowDown | Move focus to next item |
| ArrowLeft / ArrowUp | Move focus to previous item |
| Space / Enter | Toggle the focused item |
| Home | Focus first item |
| End | Focus last item |

Arrow key direction depends on `orientation`: horizontal uses Left/Right, vertical uses Up/Down.

## Accessibility

- Uses `role="group"` for the container
- Each toggle uses `aria-pressed` to indicate state
- Icon-only buttons **must** include `aria-label`
- Keyboard navigation follows ARIA authoring practices
- Focus management automatically handles arrow key navigation

## Styling Notes

### Joined Buttons (spacing=0)

When `spacing={0}`, items are visually joined:
- First item keeps left border radius (or top for vertical)
- Middle items have no border radius
- Last item keeps right border radius (or bottom for vertical)
- Outline variant: middle items share borders (no duplicate borders)

### Context Inheritance

Items automatically inherit `variant` and `size` from the parent ToggleGroup via React context. You can override these per-item if needed.

## Related

- [Toggle](./toggle.llm.md) - Single toggle button
- [Button](./button.llm.md) - Standard button component
- [RadioGroup](./radio-group.llm.md) - Radio button groups

---

# Toggle

Two-state button that can be pressed (on) or unpressed (off).

## Import

```tsx
import { Toggle } from "@neynar/ui/toggle"
```

## Anatomy

```tsx
<Toggle>
  <Icon />
  Label
</Toggle>
```

## Props

### Toggle

Extends `@base-ui/react/toggle` Props with additional variant styling.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| pressed | boolean | - | Controlled pressed state |
| defaultPressed | boolean | false | Uncontrolled default pressed state |
| onPressedChange | (pressed: boolean) => void | - | Called when pressed state changes |
| disabled | boolean | false | Whether toggle is disabled |
| variant | "default" \| "outline" | "default" | Visual style variant |
| size | "sm" \| "default" \| "lg" | "default" | Toggle size |
| children | ReactNode | - | Toggle content (icon, text, or both) |
| className | string | - | Additional CSS classes |
| render | ReactElement \| function | - | Custom render prop |

### Variant Details

**variant="default"**: Transparent background, shows pressed state with muted background.

**variant="outline"**: Border with shadow, more prominent visual separation.

## Data Attributes

| Attribute | When Present |
|-----------|--------------|
| data-pressed | Toggle is in pressed state |
| data-disabled | Toggle is disabled |
| aria-pressed | "true" or "false" based on state |

## Variants

| Variant | Options |
|---------|---------|
| variant | default, outline |
| size | sm, default, lg |

## Examples

### Basic Toggle

```tsx
function BasicExample() {
  const [pressed, setPressed] = useState(false)

  return (
    <Toggle
      pressed={pressed}
      onPressedChange={setPressed}
    >
      Toggle me
    </Toggle>
  )
}
```

### Icon-Only Toggle

```tsx
import { BoldIcon } from "lucide-react"

function IconToggle() {
  return (
    <Toggle aria-label="Toggle bold">
      <BoldIcon />
    </Toggle>
  )
}
```

### Icon with Text

```tsx
import { GridIcon, ListIcon } from "lucide-react"

function ViewModeToggle() {
  const [viewMode, setViewMode] = useState<"grid" | "list">("grid")

  return (
    <div className="flex gap-2">
      <Toggle
        pressed={viewMode === "grid"}
        onPressedChange={(pressed) =>
          pressed && setViewMode("grid")
        }
        aria-label="Grid view"
      >
        <GridIcon />
        Grid
      </Toggle>
      <Toggle
        pressed={viewMode === "list"}
        onPressedChange={(pressed) =>
          pressed && setViewMode("list")
        }
        aria-label="List view"
      >
        <ListIcon />
        List
      </Toggle>
    </div>
  )
}
```

### Formatting Toolbar

```tsx
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react"

function FormattingToolbar() {
  const [formatting, setFormatting] = useState({
    bold: false,
    italic: false,
    underline: false,
  })

  return (
    <div className="flex gap-2">
      <Toggle
        size="sm"
        variant="outline"
        pressed={formatting.bold}
        onPressedChange={(pressed) =>
          setFormatting((prev) => ({ ...prev, bold: pressed }))
        }
        aria-label="Toggle bold"
      >
        <BoldIcon />
      </Toggle>
      <Toggle
        size="sm"
        variant="outline"
        pressed={formatting.italic}
        onPressedChange={(pressed) =>
          setFormatting((prev) => ({ ...prev, italic: pressed }))
        }
        aria-label="Toggle italic"
      >
        <ItalicIcon />
      </Toggle>
      <Toggle
        size="sm"
        variant="outline"
        pressed={formatting.underline}
        onPressedChange={(pressed) =>
          setFormatting((prev) => ({ ...prev, underline: pressed }))
        }
        aria-label="Toggle underline"
      >
        <UnderlineIcon />
      </Toggle>
    </div>
  )
}
```

### Theme Toggle

```tsx
import { SunIcon, MoonIcon } from "lucide-react"

function ThemeToggle() {
  const [theme, setTheme] = useState<"light" | "dark">("light")

  return (
    <div className="flex gap-2">
      <Toggle
        variant="outline"
        pressed={theme === "light"}
        onPressedChange={(pressed) =>
          pressed && setTheme("light")
        }
        aria-label="Light theme"
      >
        <SunIcon />
        Light
      </Toggle>
      <Toggle
        variant="outline"
        pressed={theme === "dark"}
        onPressedChange={(pressed) =>
          pressed && setTheme("dark")
        }
        aria-label="Dark theme"
      >
        <MoonIcon />
        Dark
      </Toggle>
    </div>
  )
}
```

## Keyboard

| Key | Action |
|-----|--------|
| Space | Toggle pressed state |
| Enter | Toggle pressed state |

## Accessibility

- Renders semantic `<button>` element with `aria-pressed` attribute
- Icons automatically sized to 16px (size-4) unless custom size class provided
- Always provide `aria-label` when using icon-only toggles
- Disabled state prevents all interaction and reduces opacity
- Focus visible ring indicates keyboard focus

## Related

- [Button](./button.llm.md) - For actions instead of state toggles
- [Checkbox](./checkbox.llm.md) - For form selections
- [Switch](./switch.llm.md) - For on/off settings

---

# Tooltip

Displays contextual information on hover or focus.

## Import

```tsx
import { Tooltip, TooltipTrigger, TooltipContent } from "@neynar/ui/tooltip"
```

## Anatomy

```tsx
<Tooltip>
  <TooltipTrigger>Hover me</TooltipTrigger>
  <TooltipContent>Helpful information</TooltipContent>
</Tooltip>
```

## Components

| Component | Description |
|-----------|-------------|
| Tooltip | Root container, manages open state automatically |
| TooltipTrigger | Element that shows tooltip on hover/focus |
| TooltipContent | Popup content with automatic portal, positioning, and arrow |
| TooltipProvider | Optional provider for configuring shared delay (auto-included in Tooltip) |

## Props

### Tooltip

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| defaultOpen | boolean | - | Whether tooltip is initially open (uncontrolled) |
| open | boolean | - | Controlled open state |
| onOpenChange | (open: boolean) => void | - | Called when open state changes |
| trackCursorAxis | 'none' \| 'both' \| 'x' \| 'y' | 'none' | Which axis the tooltip should track the cursor on |

### TooltipTrigger

Standard trigger props with `render` prop support. Use `render` to customize:

```tsx
<TooltipTrigger render={<Button variant="ghost" size="icon" />}>
  <SettingsIcon />
</TooltipTrigger>
```

### TooltipContent

Automatically renders portal with positioner and arrow.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| side | 'top' \| 'bottom' \| 'left' \| 'right' | 'top' | Which side to position tooltip |
| sideOffset | number | 4 | Distance from trigger in pixels |
| align | 'start' \| 'center' \| 'end' | 'center' | How to align tooltip relative to side |
| alignOffset | number | 0 | Offset along alignment axis in pixels |
| className | string | - | Additional CSS classes |

### TooltipProvider

Only needed when sharing state across multiple tooltips. Tooltip automatically includes provider.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| delay | number | 0 | Delay in ms before showing tooltips |

## Data Attributes (for styling)

| Attribute | When Present |
|-----------|--------------|
| data-open | Tooltip is open |
| data-closed | Tooltip is closed |
| data-side | Current side: 'top' \| 'bottom' \| 'left' \| 'right' |
| data-align | Current alignment: 'start' \| 'center' \| 'end' |
| data-starting-style | Tooltip is animating in |
| data-ending-style | Tooltip is animating out |
| data-instant | Animations should be instant |

## Examples

### Basic Usage

```tsx
<Tooltip>
  <TooltipTrigger>
    <Button variant="outline">Hover me</Button>
  </TooltipTrigger>
  <TooltipContent>
    <p>This is a tooltip</p>
  </TooltipContent>
</Tooltip>
```

### Icon Button with Tooltip

```tsx
<Tooltip>
  <TooltipTrigger>
    <Button variant="ghost" size="icon">
      <SettingsIcon />
    </Button>
  </TooltipTrigger>
  <TooltipContent>
    <p>Settings</p>
  </TooltipContent>
</Tooltip>
```

### Placement Options

```tsx
<Tooltip>
  <TooltipTrigger>
    <Button>Show below</Button>
  </TooltipTrigger>
  <TooltipContent side="bottom" align="start">
    <p>Positioned at bottom-start</p>
  </TooltipContent>
</Tooltip>
```

### With Keyboard Shortcut

```tsx
<Tooltip>
  <TooltipTrigger>
    <Button variant="outline">Save</Button>
  </TooltipTrigger>
  <TooltipContent>
    <div className="flex items-center gap-2">
      <span>Save document</span>
      <kbd className="bg-background text-foreground rounded border px-1.5 py-0.5 font-mono text-xs">
        ⌘S
      </kbd>
    </div>
  </TooltipContent>
</Tooltip>
```

### Help Icon with Detailed Info

```tsx
<Tooltip>
  <TooltipTrigger>
    <button className="text-muted-foreground hover:text-foreground inline-flex items-center transition-colors">
      <HelpCircleIcon className="size-4" />
    </button>
  </TooltipTrigger>
  <TooltipContent side="top">
    <p className="max-w-xs">
      Track your API requests, rate limits, and usage patterns across all your applications.
    </p>
  </TooltipContent>
</Tooltip>
```

### Always Open (Controlled)

```tsx
<Tooltip defaultOpen>
  <TooltipTrigger>
    <Button variant="outline">Always visible</Button>
  </TooltipTrigger>
  <TooltipContent>
    <p>This tooltip is always visible</p>
  </TooltipContent>
</Tooltip>
```

## Keyboard

| Key | Action |
|-----|--------|
| Tab | Move focus to/from trigger |
| Escape | Close tooltip |

## Accessibility

- Automatically adds ARIA attributes for screen readers
- Tooltip appears on hover and focus for keyboard users
- Content announced to screen readers when opened
- Works with disabled elements (tooltip still shows)
- Arrow provides visual relationship between trigger and content

## Related

- [Popover](./popover.llm.md) - For interactive content requiring click to open
- [HoverCard](./hover-card.llm.md) - For rich preview content on hover

---

