{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"inline-correction","type":"registry:ui","title":"Inline Correction","description":"Fix the answer where it is wrong, in place — the edit becomes training data, so the component keeps the original alongside it rather than overwriting it.","author":"Scrim UI (https://scrimui.dev)","categories":["feedback"],"docs":"https://scrimui.dev/components/inline-correction","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/inline-correction/inline-correction.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Fix the answer where it is wrong, in place.\n *\n * A thumbs-down says something is wrong. A correction says *what the right\n * answer was*, which is worth roughly an order of magnitude more and is\n * almost never collected, because the obvious implementation destroys the\n * thing it was trying to capture.\n *\n * **Never overwrite the original.** The value in a correction is the *pair* —\n * what the model said and what a human replaced it with. An editor that swaps\n * the text in place has collected half a training example and thrown away the\n * half that identifies the failure. So `text` stays, `correction` is a second\n * field, and the reader can flip between them after saving.\n *\n * **The edit is not the message.** Correcting an answer must not send a new\n * turn — that is a different act with a different meaning, and conflating\n * them means every correction also drags the conversation forward. This\n * component emits a correction and nothing else; what the conversation does\n * next is the caller's decision.\n *\n * **Escape must not lose the work.** A textarea that discards on Escape is a\n * textarea people learn not to use. Escape asks; only an empty draft closes\n * silently.\n *\n * What is deliberately not here: rich text. A correction is a claim about\n * facts, and a formatting toolbar invites edits that are about taste, which\n * is noise in the very dataset this exists to build.\n */\n\nexport type InlineCorrectionProps = {\n  /** What the model said. Never mutated. */\n  text: string;\n  /** The accepted correction, once there is one. */\n  correction?: string;\n  /** Who corrected it, for a shared thread. */\n  correctedBy?: string;\n  onSubmit?: (corrected: string) => void;\n  /** Withdraw a correction. Should delete it server-side, not hide it. */\n  onRevert?: () => void;\n  className?: string;\n};\n\n/* ------------------------------------------------------------------ */\n/* Icons                                                               */\n/* ------------------------------------------------------------------ */\n\nfunction PencilIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"12\" height=\"12\" {...props}>\n      <path d=\"M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5z\" />\n    </svg>\n  );\n}\n\nfunction CheckIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"12\" height=\"12\" {...props}>\n      <path d=\"M20 6 9 17l-5-5\" />\n    </svg>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* InlineCorrection                                                    */\n/* ------------------------------------------------------------------ */\n\nexport function InlineCorrection({\n  text,\n  correction,\n  correctedBy,\n  onSubmit,\n  onRevert,\n  className = \"\",\n}: InlineCorrectionProps) {\n  const [editing, setEditing] = React.useState(false);\n  const [draft, setDraft] = React.useState(correction ?? text);\n  const [showOriginal, setShowOriginal] = React.useState(false);\n  const areaRef = React.useRef<HTMLTextAreaElement>(null);\n\n  React.useEffect(() => {\n    if (!editing) return;\n    const el = areaRef.current;\n    if (!el) return;\n    el.focus();\n    /* Caret at the end rather than selecting everything: a correction is\n       usually a small edit to a long paragraph, and select-all means the\n       first keystroke deletes the answer they were trying to fix. */\n    el.setSelectionRange(el.value.length, el.value.length);\n  }, [editing]);\n\n  function open() {\n    setDraft(correction ?? text);\n    setEditing(true);\n  }\n\n  function save() {\n    const next = draft.trim();\n    if (next === \"\" || next === (correction ?? text)) {\n      setEditing(false);\n      return;\n    }\n    onSubmit?.(next);\n    setEditing(false);\n  }\n\n  const shown = correction !== undefined && !showOriginal ? correction : text;\n\n  if (editing) {\n    return (\n      <div className={className}>\n        <textarea\n          ref={areaRef}\n          value={draft}\n          onChange={(e) => setDraft(e.target.value)}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\" && (e.metaKey || e.ctrlKey)) {\n              e.preventDefault();\n              save();\n            }\n            if (e.key === \"Escape\") {\n              e.preventDefault();\n              /* Only a draft that has not moved closes silently. Anything\n                 else asks — a textarea that discards on Escape is one people\n                 learn not to trust with anything long. */\n              if (draft === (correction ?? text)) setEditing(false);\n              else if (confirm(\"Discard this correction?\")) setEditing(false);\n            }\n          }}\n          rows={Math.min(12, Math.max(3, draft.split(\"\\n\").length + 1))}\n          className=\"w-full resize-y rounded-xl border border-zinc-300 bg-white px-3 py-2.5 text-[15px] leading-7 text-zinc-900 outline-none focus:border-zinc-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100 dark:focus:border-zinc-400\"\n        />\n        <div className=\"mt-2 flex items-center gap-2\">\n          <button\n            type=\"button\"\n            onClick={save}\n            className=\"inline-flex h-8 items-center gap-1.5 rounded-lg bg-zinc-900 px-3.5 text-xs font-medium text-white transition-opacity hover:opacity-90 dark:bg-zinc-100 dark:text-zinc-900\"\n          >\n            <CheckIcon />\n            Save correction\n          </button>\n          <button\n            type=\"button\"\n            onClick={() => setEditing(false)}\n            className=\"inline-flex h-8 items-center rounded-lg px-3 text-xs font-medium text-zinc-500 transition-colors hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800\"\n          >\n            Cancel\n          </button>\n          <span className=\"ml-auto text-[11px] text-zinc-400 dark:text-zinc-500\">\n            This does not send a message\n          </span>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className={`group ${className}`}>\n      <p\n        className={`whitespace-pre-wrap text-[15px] leading-7 ${\n          correction !== undefined && showOriginal\n            ? \"text-zinc-400 line-through decoration-zinc-300 dark:text-zinc-500 dark:decoration-zinc-600\"\n            : \"text-zinc-700 dark:text-zinc-200\"\n        }`}\n      >\n        {shown}\n      </p>\n\n      <div className=\"mt-1.5 flex flex-wrap items-center gap-2\">\n        {correction === undefined ? (\n          <button\n            type=\"button\"\n            onClick={open}\n            /* Visible on focus as well as hover: a hover-only edit affordance\n               is unreachable by keyboard and invisible on touch. */\n            className=\"inline-flex h-7 items-center gap-1.5 rounded-lg px-2 text-[11px] font-medium text-zinc-400 opacity-0 transition-opacity hover:bg-zinc-100 hover:text-zinc-700 focus-visible:opacity-100 group-hover:opacity-100 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\"\n          >\n            <PencilIcon />\n            Fix this\n          </button>\n        ) : (\n          <>\n            <span className=\"inline-flex items-center gap-1.5 rounded-lg bg-amber-100 px-2 py-1 text-[11px] font-medium text-amber-800 dark:bg-amber-900/40 dark:text-amber-300\">\n              <PencilIcon />\n              Corrected{correctedBy ? ` by ${correctedBy}` : \"\"}\n            </span>\n            <button\n              type=\"button\"\n              onClick={() => setShowOriginal((v) => !v)}\n              className=\"inline-flex h-7 items-center rounded-lg px-2 text-[11px] text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\"\n            >\n              {showOriginal ? \"Show correction\" : \"Show what the model said\"}\n            </button>\n            <button\n              type=\"button\"\n              onClick={open}\n              className=\"inline-flex h-7 items-center rounded-lg px-2 text-[11px] text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\"\n            >\n              Edit again\n            </button>\n            {onRevert && (\n              <button\n                type=\"button\"\n                onClick={onRevert}\n                className=\"inline-flex h-7 items-center rounded-lg px-2 text-[11px] text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\"\n              >\n                Withdraw\n              </button>\n            )}\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/inline-correction.tsx"}]}