{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "feedback",
  "registryDependencies": [
    "utils",
    "https://turnui.xyz/r/turn-button.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/feedback.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./turn-button\";\n\nexport type FeedbackValue = \"up\" | \"down\";\n\nexport type FeedbackDetails = {\n  value: FeedbackValue;\n  reasons: string[];\n  comment: string;\n};\n\nexport const DEFAULT_REASONS: Record<FeedbackValue, string[]> = {\n  up: [\"Accurate\", \"Helpful\", \"Well written\", \"Creative\"],\n  down: [\"Inaccurate\", \"Not helpful\", \"Too long\", \"Unsafe or offensive\", \"Other\"],\n};\n\nfunction Thumb({ direction, filled }: { direction: FeedbackValue; filled: boolean }) {\n  return (\n    <svg\n      viewBox=\"0 0 16 16\"\n      fill={filled ? \"currentColor\" : \"none\"}\n      stroke=\"currentColor\"\n      strokeWidth=\"1.4\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={cn(direction === \"down\" && \"rotate-180\")}\n    >\n      <path d=\"M5.5 7.25V13.5H3a.75.75 0 0 1-.75-.75v-4.75A.75.75 0 0 1 3 7.25h2.5Zm0 0 2.1-4.4a1.4 1.4 0 0 1 2.65.65v2.5h2.9a1.25 1.25 0 0 1 1.22 1.52l-.9 4a1.25 1.25 0 0 1-1.22.98H5.5\" />\n    </svg>\n  );\n}\n\n/**\n * Thumbs up/down with an optional follow-up (reasons and a comment).\n * The rating is reported immediately via onValueChange; details arrive on submit.\n */\nexport function Feedback({\n  value,\n  defaultValue = null,\n  onValueChange,\n  onSubmit,\n  details = \"down\",\n  reasons = DEFAULT_REASONS,\n  children,\n  className,\n}: {\n  value?: FeedbackValue | null;\n  defaultValue?: FeedbackValue | null;\n  /** Called on every rating change. Receives null when a rating is toggled off. */\n  onValueChange?: (value: FeedbackValue | null) => void;\n  /** Called when the follow-up form is submitted. */\n  onSubmit?: (details: FeedbackDetails) => void;\n  /** Which ratings open the follow-up form. */\n  details?: \"never\" | \"down\" | \"both\";\n  reasons?: Partial<Record<FeedbackValue, string[]>>;\n  /** Extra actions (e.g. Copy) rendered in the same row, before the rating buttons. */\n  children?: React.ReactNode;\n  className?: string;\n}) {\n  const [inner, setInner] = React.useState<FeedbackValue | null>(defaultValue);\n  const current = value !== undefined ? value : inner;\n  const [phase, setPhase] = React.useState<\"idle\" | \"form\" | \"done\">(\"idle\");\n  const [picked, setPicked] = React.useState<string[]>([]);\n  const [comment, setComment] = React.useState(\"\");\n  const panelRef = React.useRef<HTMLDivElement>(null);\n  const ratingRef = React.useRef<HTMLDivElement>(null);\n\n  const opensForm = (v: FeedbackValue) => details === \"both\" || (details === \"down\" && v === \"down\");\n\n  React.useEffect(() => {\n    if (phase === \"form\") panelRef.current?.querySelector<HTMLElement>(\"button, textarea\")?.focus();\n  }, [phase]);\n\n  function rate(next: FeedbackValue) {\n    const result = current === next ? null : next;\n    setInner(result);\n    onValueChange?.(result);\n    setPicked([]);\n    setComment(\"\");\n    setPhase(result && opensForm(result) ? \"form\" : \"idle\");\n  }\n\n  // The form unmounts while it holds focus, so hand focus back to the rating.\n  function closeForm(next: \"idle\" | \"done\") {\n    setPhase(next);\n    queueMicrotask(() => ratingRef.current?.querySelector<HTMLElement>('[aria-pressed=\"true\"]')?.focus());\n  }\n\n  function submit() {\n    if (!current) return;\n    onSubmit?.({ value: current, reasons: picked, comment: comment.trim() });\n    closeForm(\"done\");\n  }\n\n  const options = (current && (reasons[current] ?? DEFAULT_REASONS[current])) || [];\n\n  return (\n    <div className={cn(\"space-y-2\", className)}>\n      <div className=\"flex items-center gap-0.5\">\n        {children}\n        <div ref={ratingRef} className=\"flex items-center gap-0.5\" role=\"group\" aria-label=\"Rate this response\">\n          {([\"up\", \"down\"] as const).map((v) => (\n            <Button\n              key={v}\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon\"\n              aria-pressed={current === v}\n              aria-label={v === \"up\" ? \"Good response\" : \"Bad response\"}\n              onClick={() => rate(v)}\n              className=\"aria-pressed:bg-muted aria-pressed:text-foreground\"\n            >\n              <Thumb direction={v} filled={current === v} />\n            </Button>\n          ))}\n        </div>\n        <span role=\"status\" className=\"ml-2 text-xs text-muted-foreground\">\n          {phase === \"done\" ? \"Thanks for the feedback\" : \"\"}\n        </span>\n      </div>\n\n      {phase === \"form\" && current && (\n        <div ref={panelRef} className=\"w-full max-w-sm space-y-3 rounded border bg-background p-3\">\n          <div className=\"text-sm font-medium\">{current === \"up\" ? \"What did you like?\" : \"What went wrong?\"}</div>\n          {options.length > 0 && (\n            <div className=\"flex flex-wrap gap-1.5\" role=\"group\" aria-label=\"Reasons\">\n              {options.map((r) => {\n                const on = picked.includes(r);\n                return (\n                  <button\n                    key={r}\n                    type=\"button\"\n                    aria-pressed={on}\n                    onClick={() => setPicked((p) => (on ? p.filter((x) => x !== r) : [...p, r]))}\n                    className=\"rounded-sm border px-2.5 py-1 text-xs text-muted-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring aria-pressed:border-foreground aria-pressed:text-foreground\"\n                  >\n                    {r}\n                  </button>\n                );\n              })}\n            </div>\n          )}\n          <textarea\n            rows={2}\n            value={comment}\n            placeholder=\"Tell us more (optional)\"\n            aria-label=\"Additional feedback\"\n            onChange={(e) => setComment(e.target.value)}\n            onKeyDown={(e) => {\n              if (e.key === \"Enter\" && (e.metaKey || e.ctrlKey)) {\n                e.preventDefault();\n                submit();\n              }\n              if (e.key === \"Escape\") closeForm(\"idle\");\n            }}\n            className=\"w-full resize-none rounded-sm border bg-transparent px-2.5 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n          />\n          <div className=\"flex justify-end gap-2\">\n            <Button type=\"button\" size=\"sm\" variant=\"ghost\" onClick={() => closeForm(\"idle\")}>Skip</Button>\n            <Button type=\"button\" size=\"sm\" onClick={submit}>Submit</Button>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}