{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "edit-message",
  "title": "Edit Message",
  "description": "A user message with copy and edit actions, inline editing with keyboard shortcuts, and switching between edited versions.",
  "dependencies": [
    "@solar-icons/react@^2.2.0",
    "cn@^0.3.0",
    "motion@^13.4.0"
  ],
  "registryDependencies": [
    "button",
    "textarea"
  ],
  "files": [
    {
      "path": "src/components/ai/edit-message.tsx",
      "content": "import { CheckCircleIcon as CheckIcon } from '@solar-icons/react/linear/check-circle'\nimport { CloseIcon as XIcon } from '@solar-icons/react/linear/close'\nimport { AltArrowLeftIcon as ChevronLeftIcon } from '@solar-icons/react/linear/alt-arrow-left'\nimport { AltArrowRightIcon as ChevronRightIcon } from '@solar-icons/react/linear/alt-arrow-right'\nimport { CopyIcon } from '@solar-icons/react/linear/copy'\nimport { PenIcon } from '@solar-icons/react/linear/pen'\nimport { AnimatePresence, MotionConfig, motion } from 'motion/react'\nimport * as React from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { Textarea } from '@/components/ui/textarea'\nimport { cn } from 'cn'\n\ninterface EditMessageProps extends Omit<React.ComponentProps<'div'>, 'onCopy'> {\n  activeVersion?: number\n  defaultVersions?: string[]\n  draft?: string\n  editing?: boolean\n  onActiveVersionChange?: (index: number) => void\n  onCopy?: (value: string) => void\n  onDraftChange?: (value: string) => void\n  onEditingChange?: (editing: boolean) => void\n  onSave?: (value: string) => void\n  versions?: string[]\n}\n\nconst ease = [0.23, 1, 0.32, 1] as const\n\nfunction useControllable<T>(value: T | undefined, initial: T, onChange?: (next: T) => void) {\n  const [internal, setInternal] = React.useState(initial)\n  const current = value === undefined ? internal : value\n  const set = (next: T) => {\n    if (value === undefined) setInternal(next)\n    onChange?.(next)\n  }\n  return [current, set] as const\n}\n\nfunction EditMessage({\n  activeVersion,\n  className,\n  defaultVersions = [''],\n  draft,\n  editing,\n  onActiveVersionChange,\n  onCopy,\n  onDraftChange,\n  onEditingChange,\n  onSave,\n  versions,\n  ...props\n}: EditMessageProps) {\n  const editorId = React.useId()\n  const editorRef = React.useRef<HTMLTextAreaElement>(null)\n  const focusOnOpen = React.useRef(false)\n  const [internalVersions, setInternalVersions] = React.useState(defaultVersions)\n  const allVersions = versions ?? internalVersions\n  const [active, setActive] = useControllable(\n    activeVersion,\n    allVersions.length - 1,\n    onActiveVersionChange,\n  )\n  const [isEditing, setEditing] = useControllable(editing, false, onEditingChange)\n  const [value, setValue] = useControllable(draft, '', onDraftChange)\n  const [copied, setCopied] = React.useState(false)\n  const index = Math.min(active, allVersions.length - 1)\n  const message = allVersions[index] ?? ''\n\n  React.useEffect(() => {\n    if (!copied) return\n    const timer = setTimeout(() => setCopied(false), 1400)\n    return () => clearTimeout(timer)\n  }, [copied])\n\n  React.useEffect(() => {\n    if (!isEditing || !focusOnOpen.current) return\n    focusOnOpen.current = false\n    editorRef.current?.focus({ preventScroll: true })\n  }, [isEditing])\n\n  function startEditing() {\n    focusOnOpen.current = true\n    setValue(message)\n    setEditing(true)\n  }\n\n  function save() {\n    const next = value.trim()\n    if (!next || next === message) return setEditing(false)\n    if (versions === undefined) setInternalVersions([...allVersions, next])\n    setActive(allVersions.length)\n    setEditing(false)\n    onSave?.(next)\n  }\n\n  function copy() {\n    void navigator.clipboard?.writeText(message).catch(() => undefined)\n    setCopied(true)\n    onCopy?.(message)\n  }\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div\n        data-editing={isEditing}\n        data-slot=\"edit-message\"\n        className={cn(\n          'group/message flex w-full flex-col items-end font-sans text-sm tracking-[-0.05px]',\n          className,\n        )}\n        {...props}\n      >\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {isEditing ? (\n            <motion.section\n              animate={{ opacity: 1, scale: 1 }}\n              className=\"w-full origin-top-right rounded-[16px] border-0 bg-muted/45 p-3 shadow-none ring-0\"\n              exit={{ opacity: 0, scale: 0.98 }}\n              initial={{ opacity: 0, scale: 0.98 }}\n              key=\"editor\"\n              transition={{ duration: 0.2, ease }}\n            >\n              <label className=\"sr-only\" htmlFor={editorId}>\n                Edit message\n              </label>\n              <Textarea\n                ref={editorRef}\n                id={editorId}\n                placeholder=\"Edit your message…\"\n                className=\"min-h-20 resize-none border-0 bg-transparent px-2 py-2 text-sm shadow-none ring-0 focus-visible:ring-0 dark:bg-transparent\"\n                onChange={(event) => setValue(event.target.value)}\n                onFocus={(event) =>\n                  event.currentTarget.setSelectionRange(\n                    event.currentTarget.value.length,\n                    event.currentTarget.value.length,\n                  )\n                }\n                onKeyDown={(event) => {\n                  if (event.key === 'Escape') setEditing(false)\n                  if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) save()\n                }}\n                value={value}\n              />\n              <div className=\"mt-2 flex items-center justify-end gap-1.5 tracking-[-0.05px]\">\n                <Button\n                  className=\"h-7 rounded-full px-3 text-muted-foreground hover:text-muted-foreground has-data-[icon=inline-start]:pl-3\"\n                  onClick={() => setEditing(false)}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"ghost\"\n                >\n                  <XIcon data-icon=\"inline-start\" /> Cancel\n                </Button>\n                <Button\n                  className=\"h-7 rounded-full border-0 bg-primary bg-linear-to-b from-[color-mix(in_oklch,var(--primary),var(--primary-foreground)_30%)] to-primary px-3 text-primary-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_1px_2px_rgba(0,0,0,0.18)] shadow-none hover:brightness-110 has-data-[icon=inline-start]:pl-3 dark:bg-none\"\n                  disabled={!value.trim()}\n                  onClick={save}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"default\"\n                >\n                  <CheckIcon data-icon=\"inline-start\" /> Save and send\n                </Button>\n              </div>\n            </motion.section>\n          ) : (\n            <motion.div\n              animate={{ opacity: 1, scale: 1, y: 0 }}\n              className=\"flex max-w-[85%] origin-bottom-right flex-col items-end\"\n              exit={{ opacity: 0, scale: 0.97 }}\n              initial={{ opacity: 0, scale: 0.97, y: 4 }}\n              key=\"message\"\n              transition={{ duration: 0.2, ease }}\n            >\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                <motion.div\n                  animate={{ opacity: 1, filter: 'blur(0px)' }}\n                  className=\"w-fit rounded-[16px] bg-primary bg-linear-to-b from-[color-mix(in_oklch,var(--primary),var(--primary-foreground)_30%)] to-primary px-3.5 py-2.5 text-sm leading-6 whitespace-pre-wrap text-primary-foreground dark:bg-none\"\n                  exit={{ opacity: 0, filter: 'blur(2px)' }}\n                  initial={{ opacity: 0, filter: 'blur(2px)' }}\n                  key={`${index}-${message}`}\n                  transition={{ duration: 0.2, ease }}\n                >\n                  {message}\n                </motion.div>\n              </AnimatePresence>\n              <div\n                className={cn(\n                  'mt-1 flex items-center gap-0.5 text-muted-foreground transition-opacity duration-150 group-focus-within/message:opacity-100 group-hover/message:opacity-100',\n                  allVersions.length > 1 ? 'opacity-100' : 'opacity-0',\n                )}\n              >\n                {allVersions.length > 1 && (\n                  <nav aria-label=\"Message versions\" className=\"mr-1 flex items-center\">\n                    <Button\n                      aria-label=\"Previous version\"\n                      className=\"text-muted-foreground hover:text-muted-foreground\"\n                      disabled={index === 0}\n                      onClick={() => setActive(index - 1)}\n                      size=\"icon-xs\"\n                      variant=\"ghost\"\n                    >\n                      <ChevronLeftIcon />\n                    </Button>\n                    <span\n                      aria-live=\"polite\"\n                      className=\"min-w-8 text-center text-xs leading-5 tabular-nums\"\n                    >\n                      {index + 1}/{allVersions.length}\n                    </span>\n                    <Button\n                      aria-label=\"Next version\"\n                      className=\"text-muted-foreground hover:text-muted-foreground\"\n                      disabled={index === allVersions.length - 1}\n                      onClick={() => setActive(index + 1)}\n                      size=\"icon-xs\"\n                      variant=\"ghost\"\n                    >\n                      <ChevronRightIcon />\n                    </Button>\n                  </nav>\n                )}\n                <Button\n                  aria-label={copied ? 'Copied' : 'Copy message'}\n                  className=\"text-muted-foreground hover:text-muted-foreground\"\n                  onClick={copy}\n                  size=\"icon-xs\"\n                  variant=\"ghost\"\n                >\n                  <span className=\"grid *:col-start-1 *:row-start-1 *:transition-[opacity,scale] *:duration-150 motion-reduce:*:transition-opacity\">\n                    <CopyIcon className={cn(copied && 'scale-75 opacity-0')} />\n                    <CheckIcon className={cn(!copied && 'scale-75 opacity-0')} />\n                  </span>\n                </Button>\n                <Button\n                  aria-label=\"Edit message\"\n                  className=\"text-muted-foreground hover:text-muted-foreground\"\n                  onClick={startEditing}\n                  size=\"icon-xs\"\n                  variant=\"ghost\"\n                >\n                  <PenIcon />\n                </Button>\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    </MotionConfig>\n  )\n}\n\nexport { EditMessage }\nexport type { EditMessageProps }\n",
      "type": "registry:component",
      "target": "@components/ai/edit-message.tsx"
    }
  ],
  "type": "registry:component"
}