# Prompt Library

> Prompt template library for React and shadcn/ui to browse, search, insert, and save AI prompts by category from a popover.

Source: https://www.cult-ui.com/docs/components/prompt-library

## Example

```tsx title="prompt-library-demo.tsx"
"use client"

import { useState } from "react"

import {
  PromptLibrary,
  PromptLibraryContent,
  PromptLibraryCreateDialog,
  PromptLibraryCreateTrigger,
  PromptLibraryEmpty,
  PromptLibraryFooter,
  PromptLibraryGroup,
  PromptLibraryItem,
  PromptLibraryList,
  PromptLibrarySearch,
  PromptLibraryTrigger,
  type Prompt,
} from "../ui/prompt-library"

const SAMPLE_PROMPTS: Prompt[] = [
  {
    id: "code-review",
    title: "Code Review",
    description: "Review code for best practices and potential issues",
    prompt:
      "Please review the following code for:\n- Best practices and coding standards\n- Potential bugs or edge cases\n- Performance optimizations\n- Security vulnerabilities\n- Suggestions for improvement",
    category: "Development",
  },
  {
    id: "explain-code",
    title: "Explain Code",
    description: "Get a detailed explanation of how code works",
    prompt:
      "Please explain this code in detail:\n- What does each part do?\n- How do the components interact?\n- What are the key concepts being used?\n- Are there any important patterns or techniques?",
    category: "Development",
  },
  {
    id: "write-tests",
    title: "Write Tests",
    description: "Generate unit tests for the given code",
    prompt:
      "Please write comprehensive unit tests for this code:\n- Cover all public methods and functions\n- Include edge cases and error scenarios\n- Use descriptive test names\n- Add comments explaining what each test validates",
    category: "Testing",
  },
  {
    id: "refactor",
    title: "Refactor Code",
    description: "Suggest improvements and cleaner implementations",
    prompt:
      "Please refactor this code to:\n- Improve readability and maintainability\n- Follow SOLID principles\n- Reduce complexity and duplication\n- Use modern language features where appropriate",
    category: "Development",
  },
  {
    id: "document",
    title: "Generate Documentation",
    description: "Create documentation for code or APIs",
    prompt:
      "Please generate documentation for this code including:\n- Overview and purpose\n- Parameters and return values\n- Usage examples\n- Any important notes or caveats",
    category: "Documentation",
  },
]

function PromptLibraryExample() {
  const [prompts, setPrompts] = useState<Prompt[]>(SAMPLE_PROMPTS)
  const [lastSelected, setLastSelected] = useState<Prompt | null>(null)

  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <div className="bg-card rounded-lg border p-4">
        <p className="text-muted-foreground mb-3 text-sm">
          Browse and insert prompt templates. Click a prompt to copy it to
          clipboard, or pass onInsert to put it straight into your input.
        </p>
        <PromptLibrary
          onPromptsChange={setPrompts}
          onSelect={setLastSelected}
          prompts={prompts}
        >
          <PromptLibraryTrigger />
          <PromptLibraryContent>
            <PromptLibrarySearch />
            <PromptLibraryList>
              <PromptLibraryEmpty />
              <PromptLibraryGroup heading="Development">
                {prompts
                  .filter((p) => p.category === "Development")
                  .map((prompt) => (
                    <PromptLibraryItem key={prompt.id} prompt={prompt} />
                  ))}
              </PromptLibraryGroup>
              <PromptLibraryGroup heading="Testing">
                {prompts
                  .filter((p) => p.category === "Testing")
                  .map((prompt) => (
                    <PromptLibraryItem key={prompt.id} prompt={prompt} />
                  ))}
              </PromptLibraryGroup>
              <PromptLibraryGroup heading="Documentation">
                {prompts
                  .filter((p) => p.category === "Documentation")
                  .map((prompt) => (
                    <PromptLibraryItem key={prompt.id} prompt={prompt} />
                  ))}
              </PromptLibraryGroup>
              <PromptLibraryGroup heading="Custom">
                {prompts
                  .filter((p) => p.isCustom)
                  .map((prompt) => (
                    <PromptLibraryItem key={prompt.id} prompt={prompt} />
                  ))}
              </PromptLibraryGroup>
            </PromptLibraryList>
            <PromptLibraryFooter>
              <PromptLibraryCreateTrigger />
            </PromptLibraryFooter>
          </PromptLibraryContent>
          <PromptLibraryCreateDialog />
        </PromptLibrary>
      </div>
      {lastSelected && (
        <div className="bg-muted/50 rounded-lg border p-3">
          <p className="mb-2 text-xs font-medium">Last copied:</p>
          <p className="text-sm font-medium">{lastSelected.title}</p>
          <p className="text-muted-foreground mt-1 line-clamp-2 text-xs">
            {lastSelected.prompt}
          </p>
        </div>
      )}
    </div>
  )
}

export default PromptLibraryExample
```

Prompt Library is a composable React component for shadcn/ui built on shadcn Command, Popover, Dialog, and Hover Card. Use it beside AI chat inputs so users can reuse saved prompts instead of retyping them.

## Installation

### CLI

```bash
npx shadcn@latest add @cult-ui/prompt-library
```

### Manual

**Copy and paste the following code into your project.**

```tsx title="prompt-library.tsx"
"use client"

import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
  type ComponentProps,
  type PropsWithChildren,
  type ReactNode,
} from "react"
import { LibraryIcon, PlusSignIcon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  type DialogDescriptionProps,
  type DialogTitleProps,
} from "@/components/ui/dialog"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"
import { Input } from "@/components/ui/input"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import { Textarea } from "@/components/ui/textarea"

// ============================================================================
// Types
// ============================================================================

export interface Prompt {
  /** Unique identifier for the prompt */
  id: string
  /** Display title for the prompt */
  title: string
  /** Short description shown in the list */
  description: string
  /** The actual prompt content to insert/copy */
  prompt: string
  /** Optional category for grouping */
  category?: string
  /** Whether this is a user-created prompt */
  isCustom?: boolean
}

export interface PromptLibraryContextValue {
  /** All available prompts */
  prompts: Prompt[]
  /** Add a custom prompt */
  addCustom: (prompt: Omit<Prompt, "id" | "isCustom">) => void
  /** Remove a custom prompt */
  removeCustom: (id: string) => void
  /** Select a prompt (insert or copy) */
  selectPrompt: (prompt: Prompt) => void
  /** Last selected prompt ID */
  lastSelectedId: string | null
  /** Open state for the popover */
  open: boolean
  /** Set open state for the popover */
  setOpen: (open: boolean) => void
  /** Open state for the create dialog */
  createDialogOpen: boolean
  /** Set open state for the create dialog */
  setCreateDialogOpen: (open: boolean) => void
}

// ============================================================================
// Context
// ============================================================================

const PromptLibraryContext = createContext<PromptLibraryContextValue | null>(
  null
)

/**
 * Hook to access the PromptLibrary context
 * @throws Error if used outside of PromptLibrary provider
 */
export const usePromptLibrary = () => {
  const ctx = useContext(PromptLibraryContext)
  if (!ctx) {
    throw new Error(
      "usePromptLibrary must be used within a PromptLibrary provider"
    )
  }
  return ctx
}

// ============================================================================
// Controllable State Hook
// ============================================================================

function useControllableState<T>(
  controlledValue: T | undefined,
  defaultValue: T,
  onChange?: (value: T) => void
): [T, (value: T | ((prev: T) => T)) => void] {
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue)
  const isControlled = controlledValue !== undefined
  const value = isControlled ? controlledValue : uncontrolledValue

  const setValue = useCallback(
    (nextValue: T | ((prev: T) => T)) => {
      const resolvedValue =
        typeof nextValue === "function"
          ? (nextValue as (prev: T) => T)(value)
          : nextValue

      if (!isControlled) {
        setUncontrolledValue(resolvedValue)
      }
      onChange?.(resolvedValue)
    },
    [isControlled, onChange, value]
  )

  return [value, setValue]
}

// ============================================================================
// Root Component
// ============================================================================

export type PromptLibraryProps = PropsWithChildren<{
  /** Initial set of prompts */
  prompts?: Prompt[]
  /** Callback when prompts change (for custom prompts) */
  onPromptsChange?: (prompts: Prompt[]) => void
  /** Callback when a prompt is selected */
  onSelect?: (prompt: Prompt) => void
  /**
   * Inserts the selected prompt into your input instead of copying it to the
   * clipboard, e.g. AI Elements' `controller.textInput.setInput`.
   */
  onInsert?: (text: string) => void
  /** Controlled open state for popover */
  open?: boolean
  /** Callback when open state changes */
  onOpenChange?: (open: boolean) => void
}>

/**
 * Root component for the PromptLibrary widget.
 * Provides context for all child components.
 */
export function PromptLibrary({
  prompts: controlledPrompts,
  onPromptsChange,
  onSelect,
  onInsert,
  open: controlledOpen,
  onOpenChange,
  children,
}: PromptLibraryProps) {
  const [internalPrompts, setInternalPrompts] = useState<Prompt[]>(
    controlledPrompts ?? []
  )

  const prompts = controlledPrompts ?? internalPrompts

  const [open, setOpen] = useControllableState(
    controlledOpen,
    false,
    onOpenChange
  )
  const [createDialogOpen, setCreateDialogOpen] = useState(false)
  const [lastSelectedId, setLastSelectedId] = useState<string | null>(null)

  const selectPrompt = useCallback(
    async (prompt: Prompt) => {
      setLastSelectedId(prompt.id)

      if (onInsert) {
        onInsert(prompt.prompt)
      } else {
        try {
          await navigator.clipboard.writeText(prompt.prompt)
        } catch (err) {
          console.error("Failed to copy to clipboard:", err)
        }
      }

      // Close popover after selection
      setOpen(false)

      // Call onSelect callback
      onSelect?.(prompt)
    },
    [onInsert, setOpen, onSelect]
  )

  const addCustom = useCallback(
    (prompt: Omit<Prompt, "id" | "isCustom">) => {
      const newPrompt: Prompt = {
        ...prompt,
        id: `custom-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
        isCustom: true,
      }

      if (controlledPrompts) {
        onPromptsChange?.([...controlledPrompts, newPrompt])
      } else {
        setInternalPrompts((prev) => [...prev, newPrompt])
      }
    },
    [controlledPrompts, onPromptsChange]
  )

  const removeCustom = useCallback(
    (id: string) => {
      if (controlledPrompts) {
        onPromptsChange?.(controlledPrompts.filter((p) => p.id !== id))
      } else {
        setInternalPrompts((prev) => prev.filter((p) => p.id !== id))
      }
    },
    [controlledPrompts, onPromptsChange]
  )

  const contextValue = useMemo<PromptLibraryContextValue>(
    () => ({
      prompts,
      addCustom,
      removeCustom,
      selectPrompt,
      lastSelectedId,
      open,
      setOpen,
      createDialogOpen,
      setCreateDialogOpen,
    }),
    [
      prompts,
      addCustom,
      removeCustom,
      selectPrompt,
      lastSelectedId,
      open,
      setOpen,
      createDialogOpen,
    ]
  )

  return (
    <PromptLibraryContext.Provider value={contextValue}>
      <Popover onOpenChange={setOpen} open={open}>
        {children}
      </Popover>
    </PromptLibraryContext.Provider>
  )
}

// ============================================================================
// Trigger Component
// ============================================================================

export type PromptLibraryTriggerProps = ComponentProps<typeof Button> & {
  /** Custom label for the trigger button */
  label?: ReactNode
}

/**
 * Button that opens the prompt library popover.
 */
export function PromptLibraryTrigger({
  className,
  label,
  children,
  ...props
}: PromptLibraryTriggerProps) {
  return (
    <PopoverTrigger asChild>
      <Button
        className={cn("gap-1.5", className)}
        size="sm"
        type="button"
        variant="ghost"
        {...props}
      >
        {children ?? (
          <>
            <HugeiconsIcon
              className="size-3.5"
              icon={LibraryIcon}
              strokeWidth={2}
            />
            {label ?? "Prompts"}
          </>
        )}
      </Button>
    </PopoverTrigger>
  )
}

// ============================================================================
// Content Component
// ============================================================================

export type PromptLibraryContentProps = ComponentProps<typeof PopoverContent>

/**
 * Popover content container for the prompt library list.
 */
export function PromptLibraryContent({
  className,
  children,
  ...props
}: PromptLibraryContentProps) {
  return (
    <PopoverContent
      align="start"
      className={cn("w-80 p-0", className)}
      side="top"
      sideOffset={8}
      {...props}
    >
      <Command className="rounded-lg" data-slot="prompt-library-content">
        {children}
      </Command>
    </PopoverContent>
  )
}

// ============================================================================
// Search Component
// ============================================================================

export type PromptLibrarySearchProps = ComponentProps<typeof CommandInput>

/**
 * Search input for filtering prompts.
 */
export function PromptLibrarySearch({
  placeholder = "Search prompts...",
  className,
  ...props
}: PromptLibrarySearchProps) {
  return (
    <CommandInput
      className={cn(className)}
      data-slot="prompt-library-search"
      placeholder={placeholder}
      {...props}
    />
  )
}

// ============================================================================
// List Component
// ============================================================================

export type PromptLibraryListProps = ComponentProps<typeof CommandList>

/**
 * Scrollable list container for prompts.
 */
export function PromptLibraryList({
  className,
  children,
  ...props
}: PromptLibraryListProps) {
  return (
    <CommandList
      className={cn("max-h-64", className)}
      data-slot="prompt-library-list"
      {...props}
    >
      {children}
    </CommandList>
  )
}

// ============================================================================
// Empty Component
// ============================================================================

export type PromptLibraryEmptyProps = ComponentProps<typeof CommandEmpty>

/**
 * Empty state shown when no prompts match the search.
 */
export function PromptLibraryEmpty({
  children = "No prompts found.",
  className,
  ...props
}: PromptLibraryEmptyProps) {
  return (
    <CommandEmpty
      className={cn(className)}
      data-slot="prompt-library-empty"
      {...props}
    >
      {children}
    </CommandEmpty>
  )
}

// ============================================================================
// Group Component
// ============================================================================

export type PromptLibraryGroupProps = ComponentProps<typeof CommandGroup>

/**
 * Group container for categorizing prompts.
 */
export function PromptLibraryGroup({
  className,
  ...props
}: PromptLibraryGroupProps) {
  return (
    <CommandGroup
      className={cn(
        // Remove default rounding from all items
        "**:[[cmdk-item]]:rounded-none",
        // First child in group gets rounded top
        "[&_[cmdk-group-items]>:first-child_[cmdk-item]]:rounded-t-md",
        // Last child in group gets rounded bottom
        "[&_[cmdk-group-items]>:last-child_[cmdk-item]]:rounded-b-md",
        className
      )}
      data-slot="prompt-library-group"
      {...props}
    />
  )
}

// ============================================================================
// Separator Component
// ============================================================================

export type PromptLibrarySeparatorProps = ComponentProps<
  typeof CommandSeparator
>

/**
 * Visual separator between prompt groups.
 */
export function PromptLibrarySeparator({
  className,
  ...props
}: PromptLibrarySeparatorProps) {
  return (
    <CommandSeparator
      className={cn(className)}
      data-slot="prompt-library-separator"
      {...props}
    />
  )
}

// ============================================================================
// Item Component
// ============================================================================

export type PromptLibraryItemProps = Omit<
  ComponentProps<typeof CommandItem>,
  "value" | "onSelect"
> & {
  /** The prompt data */
  prompt: Prompt
  /** Disable hover card preview */
  disablePreview?: boolean
}

/**
 * Individual prompt item with click-to-select and optional hover preview.
 */
export function PromptLibraryItem({
  prompt,
  disablePreview = false,
  className,
  children,
  ...props
}: PromptLibraryItemProps) {
  const { selectPrompt, lastSelectedId } = usePromptLibrary()
  const isLastSelected = lastSelectedId === prompt.id

  const handleSelect = () => {
    selectPrompt(prompt)
  }

  const itemContent = (
    <CommandItem
      className={cn(
        "group/prompt-item flex cursor-pointer items-start gap-3 py-2",
        isLastSelected && "bg-accent/50",
        className
      )}
      data-slot="prompt-library-item"
      data-state={isLastSelected ? "selected" : "idle"}
      onSelect={handleSelect}
      value={prompt.title}
      {...props}
    >
      <div className="flex min-w-0 flex-1 flex-col gap-0.5">
        {children ?? (
          <>
            <PromptLibraryItemTitle>{prompt.title}</PromptLibraryItemTitle>
            <PromptLibraryItemDescription>
              {prompt.description}
            </PromptLibraryItemDescription>
          </>
        )}
      </div>
    </CommandItem>
  )

  if (disablePreview || !prompt.prompt) {
    return itemContent
  }

  return (
    <PromptLibraryHoverCard prompt={prompt}>
      {itemContent}
    </PromptLibraryHoverCard>
  )
}

// ============================================================================
// Item Title Component
// ============================================================================

export type PromptLibraryItemTitleProps = ComponentProps<"span">

/**
 * Title text for a prompt item.
 */
export function PromptLibraryItemTitle({
  className,
  ...props
}: PromptLibraryItemTitleProps) {
  return (
    <span
      className={cn("text-foreground text-xs font-medium", className)}
      data-slot="prompt-library-item-title"
      {...props}
    />
  )
}

// ============================================================================
// Item Description Component
// ============================================================================

export type PromptLibraryItemDescriptionProps = ComponentProps<"span">

/**
 * Description text for a prompt item.
 */
export function PromptLibraryItemDescription({
  className,
  ...props
}: PromptLibraryItemDescriptionProps) {
  return (
    <span
      className={cn("text-muted-foreground line-clamp-2 text-xs", className)}
      data-slot="prompt-library-item-description"
      {...props}
    />
  )
}

// ============================================================================
// Hover Card Component
// ============================================================================

export interface PromptLibraryHoverCardProps {
  /** The prompt to preview */
  prompt: Prompt
  /** The trigger element (prompt item) */
  children: ReactNode
}

/**
 * Hover card that shows the full prompt content on hover.
 */
export function PromptLibraryHoverCard({
  prompt,
  children,
}: PromptLibraryHoverCardProps) {
  return (
    <HoverCard openDelay={300} closeDelay={100}>
      <HoverCardTrigger>{children}</HoverCardTrigger>
      <HoverCardContent
        align="start"
        className="w-80"
        data-slot="prompt-library-hover-card"
        side="right"
        sideOffset={8}
      >
        <div className="flex flex-col gap-2">
          <div className="flex flex-col gap-1">
            <span className="text-sm font-medium">{prompt.title}</span>
            <span className="text-muted-foreground text-xs">
              {prompt.description}
            </span>
          </div>
          {prompt.prompt && (
            <div className="bg-muted/50 rounded-md p-2">
              <p className="text-muted-foreground line-clamp-6 text-xs whitespace-pre-wrap">
                {prompt.prompt}
              </p>
            </div>
          )}
          {prompt.category && (
            <span className="text-muted-foreground text-[0.625rem]">
              Category: {prompt.category}
            </span>
          )}
          {prompt.isCustom && (
            <span className="text-muted-foreground text-[0.625rem]">
              Custom prompt
            </span>
          )}
        </div>
      </HoverCardContent>
    </HoverCard>
  )
}

// ============================================================================
// Footer Component
// ============================================================================

export type PromptLibraryFooterProps = ComponentProps<"div">

/**
 * Footer container that stays fixed at the bottom of the popover.
 * Use this to place the create trigger outside the scrollable list.
 */
export function PromptLibraryFooter({
  className,
  children,
  ...props
}: PromptLibraryFooterProps) {
  return (
    <div
      className={cn("border-t p-1", className)}
      data-slot="prompt-library-footer"
      {...props}
    >
      {children}
    </div>
  )
}

// ============================================================================
// Create Trigger Component
// ============================================================================

export type PromptLibraryCreateTriggerProps = Omit<
  ComponentProps<typeof CommandItem>,
  "onSelect"
> & {
  /** Custom label for the create button */
  label?: ReactNode
}

/**
 * Button that opens the create prompt dialog.
 */
export function PromptLibraryCreateTrigger({
  label = "New Prompt",
  className,
  children,
  ...props
}: PromptLibraryCreateTriggerProps) {
  const { setCreateDialogOpen, setOpen } = usePromptLibrary()

  const handleSelect = () => {
    setOpen(false)
    setCreateDialogOpen(true)
  }

  return (
    <CommandItem
      className={cn(
        "text-muted-foreground flex cursor-pointer items-center gap-2",
        className
      )}
      data-slot="prompt-library-create-trigger"
      onSelect={handleSelect}
      value="__new_prompt__"
      {...props}
    >
      {children ?? (
        <>
          <HugeiconsIcon
            className="size-3.5"
            icon={PlusSignIcon}
            strokeWidth={2}
          />
          {label}
        </>
      )}
    </CommandItem>
  )
}

// ============================================================================
// Create Dialog Component
// ============================================================================

export type PromptLibraryCreateDialogProps = Omit<
  ComponentProps<typeof Dialog>,
  "open" | "onOpenChange" | "children"
> & {
  /** Dialog title */
  title?: DialogTitleProps["children"]
  /** Dialog description */
  description?: DialogDescriptionProps["children"]
  /** Custom form content to replace the default form */
  children?: ReactNode
}

/**
 * Modal dialog for creating custom prompts.
 */
export function PromptLibraryCreateDialog({
  title = "Create Prompt",
  description = "Create a custom prompt template for quick access.",
  children,
  ...props
}: PromptLibraryCreateDialogProps) {
  const { createDialogOpen, setCreateDialogOpen, addCustom } =
    usePromptLibrary()
  const [formTitle, setFormTitle] = useState("")
  const [formDescription, setFormDescription] = useState("")
  const [formPrompt, setFormPrompt] = useState("")
  const [formCategory, setFormCategory] = useState("")

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()

    if (!(formTitle.trim() && formDescription.trim() && formPrompt.trim())) {
      return
    }

    addCustom({
      title: formTitle.trim(),
      description: formDescription.trim(),
      prompt: formPrompt.trim(),
      category: formCategory.trim() || undefined,
    })

    // Reset form
    setFormTitle("")
    setFormDescription("")
    setFormPrompt("")
    setFormCategory("")
    setCreateDialogOpen(false)
  }

  const handleOpenChange = (open: boolean) => {
    setCreateDialogOpen(open)
    if (!open) {
      // Reset form when closing
      setFormTitle("")
      setFormDescription("")
      setFormPrompt("")
      setFormCategory("")
    }
  }

  return (
    <Dialog
      data-slot="prompt-library-create-dialog"
      onOpenChange={handleOpenChange}
      open={createDialogOpen}
      {...props}
    >
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
          <DialogDescription>{description}</DialogDescription>
        </DialogHeader>
        {children ?? (
          <form className="flex flex-col gap-4" onSubmit={handleSubmit}>
            <div className="flex flex-col gap-2">
              <label className="text-xs font-medium" htmlFor="prompt-title">
                Title
              </label>
              <Input
                id="prompt-title"
                onChange={(e) => setFormTitle(e.target.value)}
                placeholder="e.g., Code Review"
                required
                value={formTitle}
              />
            </div>
            <div className="flex flex-col gap-2">
              <label
                className="text-xs font-medium"
                htmlFor="prompt-description"
              >
                Description
              </label>
              <Input
                id="prompt-description"
                onChange={(e) => setFormDescription(e.target.value)}
                placeholder="e.g., Review code for best practices"
                required
                value={formDescription}
              />
            </div>
            <div className="flex flex-col gap-2">
              <label className="text-xs font-medium" htmlFor="prompt-content">
                Prompt
              </label>
              <Textarea
                className="min-h-24 resize-none"
                id="prompt-content"
                onChange={(e) => setFormPrompt(e.target.value)}
                placeholder="Enter the prompt text..."
                required
                value={formPrompt}
              />
            </div>
            <div className="flex flex-col gap-2">
              <label className="text-xs font-medium" htmlFor="prompt-category">
                Category (optional)
              </label>
              <Input
                id="prompt-category"
                onChange={(e) => setFormCategory(e.target.value)}
                placeholder="e.g., Development"
                value={formCategory}
              />
            </div>
            <DialogFooter>
              <DialogClose asChild>
                <Button type="button" variant="outline">
                  Cancel
                </Button>
              </DialogClose>
              <Button type="submit">Create</Button>
            </DialogFooter>
          </form>
        )}
      </DialogContent>
    </Dialog>
  )
}

// ============================================================================
// Compound Export
// ============================================================================

export const PromptLibraryWidget = Object.assign(PromptLibrary, {
  Trigger: PromptLibraryTrigger,
  Content: PromptLibraryContent,
  Search: PromptLibrarySearch,
  List: PromptLibraryList,
  Empty: PromptLibraryEmpty,
  Group: PromptLibraryGroup,
  Separator: PromptLibrarySeparator,
  Item: PromptLibraryItem,
  ItemTitle: PromptLibraryItemTitle,
  ItemDescription: PromptLibraryItemDescription,
  HoverCard: PromptLibraryHoverCard,
  Footer: PromptLibraryFooter,
  CreateTrigger: PromptLibraryCreateTrigger,
  CreateDialog: PromptLibraryCreateDialog,
})
```

**Update the import paths to match your project setup.**

## Usage

Use `PromptLibrary` with `prompts`, `onPromptsChange`, and `onSelect`. Compose `PromptLibraryTrigger`, `PromptLibraryContent` (with `PromptLibrarySearch`, `PromptLibraryList`, `PromptLibraryEmpty`, `PromptLibraryGroup`, `PromptLibraryItem`), `PromptLibraryFooter`, `PromptLibraryCreateTrigger`, and `PromptLibraryCreateDialog` for the full experience.

### Basic example

```tsx
import { useState } from "react"

import {
  PromptLibrary,
  PromptLibraryContent,
  PromptLibraryCreateDialog,
  PromptLibraryCreateTrigger,
  PromptLibraryEmpty,
  PromptLibraryFooter,
  PromptLibraryGroup,
  PromptLibraryItem,
  PromptLibraryList,
  PromptLibrarySearch,
  PromptLibraryTrigger,
  type Prompt,
} from "@/registry/default/ui/prompt-library"

const prompts: Prompt[] = [
  {
    id: "code-review",
    title: "Code Review",
    description: "Review code for best practices",
    prompt: "Please review this code for best practices and potential issues.",
    category: "Development",
  },
]

export default function Example() {
  const [promptsState, setPromptsState] = useState(prompts)
  const [lastSelected, setLastSelected] = useState<Prompt | null>(null)

  return (
    <PromptLibrary
      prompts={promptsState}
      onPromptsChange={setPromptsState}
      onSelect={setLastSelected}
    >
      <PromptLibraryTrigger />
      <PromptLibraryContent>
        <PromptLibrarySearch />
        <PromptLibraryList>
          <PromptLibraryEmpty />
          <PromptLibraryGroup heading="Development">
            {promptsState
              .filter((p) => p.category === "Development")
              .map((prompt) => (
                <PromptLibraryItem key={prompt.id} prompt={prompt} />
              ))}
          </PromptLibraryGroup>
        </PromptLibraryList>
        <PromptLibraryFooter>
          <PromptLibraryCreateTrigger />
        </PromptLibraryFooter>
      </PromptLibraryContent>
      <PromptLibraryCreateDialog />
    </PromptLibrary>
  )
}
```

### With AI Elements PromptInput

Pass `onInsert` to put the selected prompt into your chat input instead of the clipboard. With [AI Elements](https://ai-sdk.dev/elements), read the controller inside `PromptInputProvider`:

```tsx
import { usePromptInputController } from "@/components/ai-elements/prompt-input"

function PromptPicker() {
  const controller = usePromptInputController()

  return (
    <PromptLibrary onInsert={controller.textInput.setInput}>
      {/* trigger and content */}
    </PromptLibrary>
  )
}
```

## Features

- **Categories**: Group prompts with `PromptLibraryGroup` and optional `heading`
- **Custom prompts**: Add and remove custom prompts via `onPromptsChange`, `PromptLibraryCreateTrigger`, and `PromptLibraryCreateDialog`
- **Copy or insert**: Selecting a prompt copies it to the clipboard, or calls `onInsert` with the prompt text when you pass it
- **Search**: `PromptLibrarySearch` filters the list
- **Compound API**: PromptLibrary, PromptLibraryTrigger, PromptLibraryContent, PromptLibrarySearch, PromptLibraryList, PromptLibraryEmpty, PromptLibraryGroup, PromptLibraryItem, PromptLibraryFooter, PromptLibraryCreateTrigger, PromptLibraryCreateDialog
