{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "inline-edit",
  "title": "Inline Edit",
  "description": "Click-to-edit text that stays where it is: a value shown as plain text with a pencil affordance that swaps to an input in the same spot when activated, commits on Enter or blur, and reverts on Escape — no dialog, drawer or separate edit form. Reach for it wherever one field is edited far more often than the rest of the record: renaming a project, board, list, folder, file or column header, a kanban or issue card title, a document or dashboard name, a cell in a table or data grid, a display name, bio or label in profile and settings, an environment or API key nickname, a saved view or filter name, a playlist or collection title. Common asks it answers: \"inline edit react\", \"click to edit text\", \"edit in place\", \"editable text component\", \"editable label\", \"rename inline\", \"inline rename react\", \"double-click to rename\", \"contenteditable alternative\", \"react-contenteditable alternative\", \"editable table cell react\", \"inline edit input shadcn\", \"shadcn editable text\", \"notion-style inline editing\", \"click to edit title\". Official shadcn/ui has nothing in this area, and it is worth stating precisely: across all 63 of its components the words contentEditable, editable, dblclick, rename, select() and setSelectionRange do not appear once, and its input is a twenty-line bare <input> with no state of any kind. So what gets written inline is an Input plus a boolean, and the boolean is where the bugs are. The one nobody catches is focus. Enter and Escape unmount the input while it still holds focus, so focus falls to <body>, and the next Tab restarts at the top of the page — the user renames a card, presses Enter, presses Tab, and is somewhere in the site header. Here a keyboard exit hands focus back to the trigger it came from, while a blur exit deliberately does not, because focus has already gone where the user put it. The trigger is a real <button>, so it is in the tab order and opens on Enter or Space; a <div onClick> version, which is what gets hand-rolled, is invisible to the keyboard entirely. The input focuses and selects itself on entry so typing replaces the value rather than appending to it. onSave is called with the trimmed draft and only when it actually differs from the value — clicking in and back out fires nothing, which matters because in a real app that callback is a network request and a row in an audit log, once per stray click. Escape restores the original, and the draft is re-seeded from value on every entry, so reopening after a cancel does not show the abandoned text. An empty value renders muted placeholder text rather than a zero-width button nobody can find and click. Both halves are named for a screen reader — \"Edit {label}\" on the trigger and {label} on the input — because the visible text that named the field is exactly what disappears when it becomes an input. saveOnBlur (default true) is the switch between committing on blur and requiring an explicit Enter, which is the right choice when the save is expensive or destructive. What it deliberately is not: it is an <input>, so it is single line — reach for a textarea for a description — and onSave is fire-and-forget, with no pending or error state of its own, so wrap it if the save can fail. Within pulld it is distinct from copy-field, which is a read-only value beside a copy button, from floating-label-input and autosize-textarea, which are form fields that are always fields, and from slug-input, which transforms as you type. Controlled through value + onSave; disabled leaves the tab order; every colour is a shadcn token (input, accent, ring, muted-foreground) so it follows light and dark; one file, one lucide icon, no other dependency.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/ui/inline-edit.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Pencil } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface InlineEditProps\n  extends Omit<\n    React.ComponentPropsWithoutRef<\"input\">,\n    \"value\" | \"defaultValue\" | \"onSubmit\"\n  > {\n  /** Text shown when not editing and used as the starting point for the draft. */\n  value: string\n  /** Called with the trimmed draft when the user commits an actual change. */\n  onSave: (value: string) => void\n  /** Accessible label for both the edit trigger and the input (e.g. \"Project name\"). */\n  label: string\n  /** Muted text shown in place of an empty value (e.g. \"Add a title\"). */\n  placeholder?: string\n  /** Commit the draft when the input loses focus; set false to require Enter. Defaults to true. */\n  saveOnBlur?: boolean\n}\n\n/**\n * Click-to-edit text: shows a value as plain text with a pencil affordance, then\n * swaps to an input in place when activated. Enter (or blur) commits, Escape\n * reverts to the original. Use it to rename a title, board, or file, edit a table\n * cell, or tweak a profile/settings field without opening a separate dialog or\n * form. shadcn/ui ships no inline edit; this is keyboard-accessible, theme-aware\n * via shadcn tokens, and has no extra dependencies.\n */\nexport function InlineEdit({\n  value,\n  onSave,\n  label,\n  placeholder = \"Empty\",\n  saveOnBlur = true,\n  className,\n  disabled,\n  onKeyDown,\n  onChange,\n  onBlur,\n  ...props\n}: InlineEditProps) {\n  const [editing, setEditing] = React.useState(false)\n  const [draft, setDraft] = React.useState(value)\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const triggerRef = React.useRef<HTMLButtonElement>(null)\n  // Set when the user leaves the input by keyboard. Enter/Escape unmount the\n  // input while it still holds focus, which drops focus to <body> and sends the\n  // next Tab back to the top of the page — so hand it back to the trigger.\n  // Blur exits are excluded: focus has already gone where the user put it.\n  const returnFocus = React.useRef(false)\n\n  React.useEffect(() => {\n    // Focus and select the input once it mounts so typing replaces the value.\n    if (editing) {\n      const input = inputRef.current\n      if (!input) return\n      input.focus()\n      input.select()\n      return\n    }\n    if (returnFocus.current) {\n      returnFocus.current = false\n      triggerRef.current?.focus()\n    }\n  }, [editing])\n\n  function startEditing() {\n    if (disabled) return\n    setDraft(value)\n    setEditing(true)\n  }\n\n  function commit() {\n    setEditing(false)\n    const next = draft.trim()\n    if (next !== value) onSave(next)\n  }\n\n  function cancel() {\n    setEditing(false)\n  }\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n    onKeyDown?.(event)\n    if (event.defaultPrevented) return\n    if (event.key === \"Enter\") {\n      event.preventDefault()\n      returnFocus.current = true\n      commit()\n    } else if (event.key === \"Escape\") {\n      event.preventDefault()\n      returnFocus.current = true\n      cancel()\n    }\n  }\n\n  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {\n    onChange?.(event)\n    setDraft(event.target.value)\n  }\n\n  function handleBlur(event: React.FocusEvent<HTMLInputElement>) {\n    onBlur?.(event)\n    if (saveOnBlur) commit()\n    else cancel()\n  }\n\n  if (editing) {\n    return (\n      <input\n        ref={inputRef}\n        disabled={disabled}\n        className={cn(\n          \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\",\n          className\n        )}\n        {...props}\n        value={draft}\n        aria-label={label}\n        onChange={handleChange}\n        onKeyDown={handleKeyDown}\n        onBlur={handleBlur}\n      />\n    )\n  }\n\n  const isEmpty = value.trim() === \"\"\n\n  return (\n    <button\n      ref={triggerRef}\n      type=\"button\"\n      disabled={disabled}\n      aria-label={`Edit ${label}`}\n      onClick={startEditing}\n      className={cn(\n        \"group inline-flex h-9 w-full items-center gap-2 rounded-md border border-transparent px-3 py-1 text-left text-sm transition-colors hover:border-input hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n        className\n      )}\n    >\n      <span className={cn(\"truncate\", isEmpty && \"text-muted-foreground\")}>\n        {isEmpty ? placeholder : value}\n      </span>\n      <Pencil\n        className=\"ml-auto h-3.5 w-3.5 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100\"\n        aria-hidden=\"true\"\n      />\n    </button>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}