{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-feedback",
  "title": "AI feedback",
  "description": "Asks what was wrong after a thumbs-down on an AI answer — one reason to choose from a short list, an optional note, and Send — opening under the answer so what is being judged stays in view.",
  "registryDependencies": [
    "button",
    "select",
    "textarea",
    "https://www.quilldesignsystem.com/r/icon.json"
  ],
  "files": [
    {
      "path": "registry/lib/ai-feedback.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { Textarea } from '@/components/ui/textarea'\nimport { Icon } from '@/components/ui/icon'\nimport { cn } from '@/lib/utils'\n\nexport type AiFeedbackValue = { reason: string | null; note: string }\n\n/** The reasons offered when the app does not bring its own. About six is the most people will read. */\nexport const FEEDBACK_REASONS = ['Wrong or made up', \"Didn't answer my question\", 'Missed something', 'Too long', 'Wrong tone', 'Other']\n\nexport type AiFeedbackFormProps = {\n  /** The question at the top. It also names the form for screen readers. */\n  title?: string\n  /** Short reasons to choose one from. Pass your own to replace the defaults; an empty list leaves only the note. A label that repeats is offered once. */\n  reasons?: string[]\n  /** Receives the chosen reason (null when only a note was written) and the note with its ends trimmed. */\n  onSubmit: (value: AiFeedbackValue) => void\n  /** Shows Close in the corner. */\n  onClose?: () => void\n  /** Names the note field and is its placeholder. */\n  noteLabel?: string\n  /** One line beside Send saying what goes with the feedback. Pass '' to leave it out. */\n  disclosure?: string\n  submitLabel?: string\n  /** Controlled: true swaps the form for the thank-you line. Leave it out and the form does that itself after Send. */\n  sent?: boolean\n  sentMessage?: string\n  className?: string\n}\n\n/** Asks what was wrong after a thumbs-down on an AI answer — one reason to choose from a short list, an optional note, and Send — opening under the answer so what is being judged stays in view. */\nexport function AiFeedbackForm({\n  title = 'What was wrong?', reasons = FEEDBACK_REASONS, onSubmit, onClose, noteLabel = 'Tell us more (optional)',\n  disclosure = 'Sends this answer with your note.', submitLabel = 'Send feedback', sent, sentMessage = 'Thanks, that helps.', className,\n}: AiFeedbackFormProps) {\n  const titleId = React.useId()\n  // A select tells options apart by value, so a label that repeats is offered once, the first staying.\n  const options = React.useMemo(() => [...new Set(reasons)], [reasons])\n  const [reason, setReason] = React.useState<string | null>(null)\n  const [note, setNote] = React.useState('')\n  const [ownSent, setOwnSent] = React.useState(false)\n  const isSent = sent === undefined ? ownSent : sent\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const statusRef = React.useRef<HTMLDivElement>(null)\n  const wasSent = React.useRef(isSent)\n  // Set when the person presses this form's own Send. \"Sent\" can also arrive from the app, and then the cursor is theirs.\n  const sentHere = React.useRef(false)\n  // Text is never required, and spaces are not text.\n  const ready = reason !== null || note.trim().length > 0\n\n  // Send removes the button that had focus. Keep the person's place on the line that replaced it, but only after a\n  // press in this form, and only if they have not moved on since. preventScroll: the line is where the button was.\n  React.useEffect(() => {\n    if (isSent && !wasSent.current && sentHere.current) {\n      const active = document.activeElement\n      if (!active || active === document.body || rootRef.current?.contains(active)) statusRef.current?.focus({ preventScroll: true })\n    }\n    if (isSent) sentHere.current = false\n    wasSent.current = isSent\n  }, [isSent])\n\n  // A press or focus move outside the form after Send means the person has moved on, so a late \"sent\" from the app must not take the cursor.\n  React.useEffect(() => {\n    const away = (event: Event) => {\n      if (!(event.target instanceof Node) || !rootRef.current?.contains(event.target)) sentHere.current = false\n    }\n    document.addEventListener('pointerdown', away, true)\n    return () => document.removeEventListener('pointerdown', away, true)\n  }, [])\n\n  const send = () => {\n    if (!ready) return\n    sentHere.current = true\n    if (sent === undefined) setOwnSent(true)\n    onSubmit({ reason, note: note.trim() })\n  }\n\n  return (\n    <div ref={rootRef} data-slot=\"ai-feedback\" onBlur={(event) => { if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget as Node)) sentHere.current = false }} role={isSent ? undefined : 'group'} aria-labelledby={isSent ? undefined : titleId}\n      className={cn('text-sm', !isSent && 'grid grid-cols-[minmax(0,1fr)] gap-2.5 rounded-xl border border-border bg-card px-4 py-3.5 shadow-sm', className)}>\n      {isSent ? null : (\n        <>\n          <div className=\"flex items-center gap-2\">\n            <p id={titleId} className=\"min-w-0 flex-1 font-semibold break-words text-foreground\">{title}</p>\n            {onClose ? (\n              <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className=\"-my-1 -mr-1.5 text-muted-foreground\" aria-label=\"Close\" onClick={onClose}><Icon name=\"close\" /></Button>\n            ) : null}\n          </div>\n          {options.length ? (\n            // Named by the question, so a screen reader hears \"What was wrong?\" on the field. alignItemWithTrigger off:\n            // the list opens under the field and never covers it or the question above.\n            <Select value={reason} onValueChange={(next) => setReason(next)}>\n              <SelectTrigger aria-labelledby={titleId} className=\"w-full bg-background\">\n                <SelectValue placeholder=\"Choose a reason\" />\n              </SelectTrigger>\n              <SelectContent alignItemWithTrigger={false}>\n                {options.map((option) => <SelectItem key={option} value={option}>{option}</SelectItem>)}\n              </SelectContent>\n            </Select>\n          ) : null}\n          <Textarea aria-label={noteLabel} placeholder={noteLabel} value={note} onChange={(event) => setNote(event.target.value)} className=\"min-h-16 bg-background\" />\n          <div className=\"flex flex-wrap items-center gap-2\">\n            {disclosure ? <p className=\"min-w-0 flex-1 text-xs text-muted-foreground\">{disclosure}</p> : <span className=\"flex-1\" />}\n            <Button type=\"button\" disabled={!ready} onClick={send}>{submitLabel}</Button>\n          </div>\n        </>\n      )}\n      {/* On the page from the start so the thank-you arrives into a live region. While empty it is absolutely\n          positioned: a real box that takes no grid row and no gap. */}\n      <div ref={statusRef} role=\"status\" tabIndex={-1} className=\"rounded-md outline-hidden focus-visible:ring-3 focus-visible:ring-ring/50 empty:absolute not-empty:w-fit not-empty:max-w-[calc(100%+0.75rem)] not-empty:-mx-1.5 not-empty:px-1.5 not-empty:py-0.5\">\n        {isSent ? <p className=\"text-xs break-words text-muted-foreground\">{sentMessage}</p> : null}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/ai-feedback.tsx"
    }
  ],
  "docs": "Asks what was wrong after a thumbs-down on an AI answer — one reason to choose from a short list, an optional note, and Send — opening under the answer so what is being judged stays in view. Do: Offer about six short reasons, your own or the defaults. Don't: List every reason you can think of — a long dropdown is as unread as a wall of chips. Do: Let Send work with a reason alone, or a note alone. Don't: Force people to type before they can send — most will leave instead. Do: Open the form under the answer it is about (pass it to AiMessage as feedbackForm). Don't: Cover the answer with a dialog — people need to see what they are judging. Do: Keep the line that says what is sent with the feedback, in your own words if the defaults are not true for your app. Don't: Send the conversation along without saying so. Use ai-message when the thumbs alone are all you collect; they are already on the reply. Use dialog when the feedback is a longer survey that needs the person's whole attention. Use contact-form when people are writing to your team, not rating one answer.",
  "type": "registry:ui"
}