{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "inline-edit",
  "title": "Inline edit",
  "description": "Text that turns into a field in place, saved with Enter and cancelled with Escape.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/inline-edit/inline-edit.tsx",
      "content": "// Based on Watermelon UI's “Inline edit” (MIT, © 2026 Watermelon Platform Contributors,\n// github.com/WatermelonCorp/watermelon-platform). Rewritten as one reusable field: Enter saves, Esc cancels, and cancel restores the value.\nimport { Check, Pencil, X } from 'lucide-react'\nimport { AnimatePresence, MotionConfig, motion } from 'motion/react'\nimport { useEffect, useId, useRef, useState, type ReactNode } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type InlineEditProps = {\n  label: string\n  value: string\n  onSave: (value: string) => void\n  icon?: ReactNode\n  multiline?: boolean\n  /** Shown when the value is empty. */\n  placeholder?: string\n  editLabel?: string\n  saveLabel?: string\n  cancelLabel?: string\n  className?: string\n}\n\n/** A value that reads like text until you edit it. The pencil swaps for save and cancel. */\nexport function InlineEdit({\n  label,\n  value,\n  onSave,\n  icon,\n  multiline = false,\n  placeholder = 'Empty',\n  editLabel = 'Edit',\n  saveLabel = 'Save',\n  cancelLabel = 'Cancel',\n  className,\n}: InlineEditProps) {\n  const [editing, setEditing] = useState(false)\n  const [draft, setDraft] = useState(value)\n  const field = useRef<HTMLInputElement & HTMLTextAreaElement>(null)\n  const pencil = useRef<HTMLButtonElement>(null)\n  const id = useId()\n\n  useEffect(() => {\n    if (editing) field.current?.select()\n  }, [editing])\n\n  const start = () => {\n    setDraft(value)\n    setEditing(true)\n  }\n  const stop = (save: boolean) => {\n    if (save && draft.trim() !== value) onSave(draft.trim())\n    setEditing(false)\n    requestAnimationFrame(() => pencil.current?.focus())\n  }\n\n  const swap = { initial: { opacity: 0, y: 24 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: 24 } }\n  const Field = multiline ? 'textarea' : 'input'\n\n  return (\n    <MotionConfig reducedMotion=\"user\" transition={{ duration: 0.35, ease: [0.19, 1, 0.22, 1] }}>\n      <div className={cn('flex flex-col gap-1 sm:flex-row sm:gap-4', multiline ? 'sm:items-start' : 'sm:items-center', className)}>\n        <label htmlFor={editing ? id : undefined} className=\"flex shrink-0 items-center gap-2.5 text-sm text-muted-foreground sm:w-32 sm:py-2.5 [&_svg]:size-4.5\">\n          {icon}\n          {label}\n        </label>\n        <div\n          className={cn(\n            'group flex min-h-11 w-full min-w-0 gap-2 overflow-hidden rounded-xl px-3 transition-colors duration-150',\n            editing ? 'bg-muted' : 'hover:bg-muted/60',\n            multiline ? 'items-start py-2' : 'items-center',\n          )}\n        >\n          {editing ? (\n            <Field\n              ref={field}\n              id={id}\n              value={draft}\n              rows={multiline ? 3 : undefined}\n              onChange={(e) => setDraft(e.target.value)}\n              onKeyDown={(e) => {\n                if (e.key === 'Escape') stop(false)\n                if (e.key === 'Enter' && (!multiline || e.metaKey || e.ctrlKey)) {\n                  e.preventDefault()\n                  stop(true)\n                }\n              }}\n              className=\"w-full min-w-0 resize-none bg-transparent font-medium outline-none\"\n            />\n          ) : (\n            <p className={cn('w-full min-w-0 font-medium', multiline ? 'whitespace-pre-line' : 'truncate', !value && 'text-muted-foreground')}>\n              {value || placeholder}\n            </p>\n          )}\n          <div className=\"flex shrink-0 items-center\">\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              {editing ? (\n                <motion.div key=\"edit\" {...swap} className=\"flex gap-1\">\n                  <button\n                    type=\"button\"\n                    aria-label={saveLabel}\n                    onClick={() => stop(true)}\n                    className=\"grid size-8 place-items-center rounded-lg bg-foreground text-background active:scale-95\"\n                  >\n                    <Check className=\"size-4\" aria-hidden />\n                  </button>\n                  <button\n                    type=\"button\"\n                    aria-label={cancelLabel}\n                    onClick={() => stop(false)}\n                    className=\"grid size-8 place-items-center rounded-lg bg-card text-foreground shadow-sm active:scale-95\"\n                  >\n                    <X className=\"size-4\" aria-hidden />\n                  </button>\n                </motion.div>\n              ) : (\n                <motion.button\n                  key=\"view\"\n                  ref={pencil}\n                  {...swap}\n                  type=\"button\"\n                  aria-label={`${editLabel} ${label.toLowerCase()}`}\n                  onClick={start}\n                  className=\"grid size-8 place-items-center rounded-lg border bg-card text-muted-foreground opacity-100 shadow-sm transition-opacity duration-150 focus-visible:opacity-100 sm:opacity-0 sm:group-hover:opacity-100\"\n                >\n                  <Pencil className=\"size-3.5\" aria-hidden />\n                </motion.button>\n              )}\n            </AnimatePresence>\n          </div>\n        </div>\n      </div>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "credit": "Adapted from Watermelon UI (MIT)."
  },
  "categories": [
    "controls"
  ],
  "type": "registry:component"
}