{
  "name": "inline-edit",
  "type": "registry:ui",
  "title": "InlineEdit",
  "description": "The commit-once exception to Ink: create-inputs and rename overlays inside navigating links, de-chromed to match Ink at rest. The input wears the display's exact box.",
  "dependencies": [
    "@candlelight-ds/tokens",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/ui/InlineEdit.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\n/**\n * InlineEdit — the commit-once exception to Ink (spec §4). Living text\n * seats are Ink; what legitimately remains here is the commit-once\n * family — CREATE inputs (typing then Enter creates; continuous save\n * would create on idle) and rename overlays inside navigating links,\n * where an editor can't sit under a click-through. Seamless: the input\n * wears the display's exact box — no border, no fill, no padding delta;\n * the caret is the only mode (G8).\n *\n * Hand-rolled contentEditable stays banned; this is input/textarea-based\n * with display-font passthrough.\n *\n * Use when: create-by-naming flows; renames overlaid on a link.\n * Don't use when: any text that lives (Ink — line/text/prose).\n */\n\nimport { useEffect, useRef, useState } from 'react'\nimport { cn } from '@/lib/utils'\n\ninterface InlineEditProps {\n  value: string\n  onCommit: (value: string) => void\n  /** Controlled editing state (e.g., entered via Menu \"Rename\" or the R key). */\n  editing?: boolean\n  onEditingChange?: (editing: boolean) => void\n  /** Double-click the text to start editing (default true). */\n  editOnDoubleClick?: boolean\n  /** Commit empty values too (clearable fields like descriptions). */\n  allowEmpty?: boolean\n  multiline?: boolean\n  placeholder?: string\n  /** Classes applied to BOTH the text and the input so type never shifts. */\n  textClassName?: string\n  className?: string\n  disabled?: boolean\n}\n\nexport function InlineEdit({\n  value,\n  onCommit,\n  editing: editingProp,\n  onEditingChange,\n  editOnDoubleClick = true,\n  allowEmpty = false,\n  multiline = false,\n  placeholder,\n  textClassName,\n  className,\n  disabled = false,\n}: InlineEditProps) {\n  const [editingState, setEditingState] = useState(false)\n  const editing = editingProp ?? editingState\n  const [draft, setDraft] = useState(value)\n  const inputRef = useRef<HTMLInputElement & HTMLTextAreaElement>(null)\n  const committedRef = useRef(false)\n\n  const setEditing = (next: boolean) => {\n    setEditingState(next)\n    onEditingChange?.(next)\n  }\n\n  useEffect(() => {\n    if (editing) {\n      committedRef.current = false\n      // eslint-disable-next-line react-hooks/set-state-in-effect -- entering edit resets the draft to the committed value: one deliberate cascade on the editing flip\n      setDraft(value)\n      // Focus after the input mounts.\n      requestAnimationFrame(() => {\n        inputRef.current?.focus()\n        inputRef.current?.select()\n      })\n    }\n  }, [editing, value])\n\n  const commit = () => {\n    if (committedRef.current) return\n    committedRef.current = true\n    const trimmed = draft.trim()\n    if ((trimmed || allowEmpty) && trimmed !== value) {\n      onCommit(trimmed)\n    }\n    setEditing(false)\n  }\n\n  const cancel = () => {\n    committedRef.current = true\n    setDraft(value)\n    setEditing(false)\n  }\n\n  if (!editing) {\n    return (\n      <span\n        onDoubleClick={\n          editOnDoubleClick && !disabled\n            ? (event) => {\n                event.stopPropagation()\n                setEditing(true)\n              }\n            : undefined\n        }\n        className={cn(\n          // `block` so truncate/wrap actually clip: overflow is inert on\n          // inline boxes, and only flex parents blockify spans on their own.\n          // The pb/-mb pair gives descender ink room: overflow clips at the\n          // padding box, so tight display leading keeps its g/y/p tails\n          // while the layout box stays the same height.\n          'block min-w-0',\n          multiline ? 'whitespace-pre-wrap break-words' : 'truncate pb-[0.2em] -mb-[0.2em]',\n          textClassName,\n          className\n        )}\n      >\n        {value || <span className=\"text-ink-faint\">{placeholder}</span>}\n      </span>\n    )\n  }\n\n  const sharedProps = {\n    ref: inputRef,\n    value: draft,\n    placeholder,\n    onChange: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>\n      setDraft(event.target.value),\n    onBlur: commit,\n    onKeyDown: (event: React.KeyboardEvent) => {\n      event.stopPropagation()\n      if (event.key === 'Enter' && (!multiline || !event.shiftKey)) {\n        event.preventDefault()\n        commit()\n      }\n      if (event.key === 'Escape') {\n        event.preventDefault()\n        cancel()\n      }\n    },\n    onClick: (event: React.MouseEvent) => event.stopPropagation(),\n    className: cn(\n      // Seamless: the input wears the display's exact box — no border,\n      // no fill, no padding delta. The caret is the only mode; nothing\n      // jumps, no container appears.\n      'w-full min-w-0 border-0 bg-transparent p-0',\n      // Same descender room the display span gets (single-line inputs\n      // clip glyph ink at tight display leading too).\n      !multiline && 'pb-[0.2em] -mb-[0.2em]',\n      // Multiline grows with its words — no inner scroll, no fixed rows.\n      multiline && 'resize-none [field-sizing:content]',\n      'outline-none placeholder:text-ink-faint',\n      textClassName,\n      className\n    ),\n    'data-1p-ignore': true,\n  }\n\n  return multiline ? (\n    <textarea {...sharedProps} rows={1} />\n  ) : (\n    <input {...sharedProps} type=\"text\" />\n  )\n}\n",
      "target": "components/ui/InlineEdit.tsx"
    }
  ],
  "meta": {
    "candlelight": {
      "category": "primitive",
      "gRules": [
        "G8"
      ],
      "useWhen": "Create-by-naming flows (typing then Enter creates) and renames overlaid on a link, where continuous save would create-on-idle.",
      "dontUseWhen": "Any text that lives — that's Ink.",
      "states": [
        "display (truncating block)",
        "editing (seamless input)",
        "empty (placeholder)"
      ],
      "keyboard": [
        {
          "keys": "Enter",
          "does": "commits once"
        },
        {
          "keys": "Esc",
          "does": "cancels, restores the display"
        },
        {
          "keys": "double-click",
          "does": "enters editing (optional)"
        }
      ],
      "acceptanceTests": [
        "No border, fill, or padding delta between display and input — the caret is the only mode.",
        "The display face is a block so truncation actually clips (overflow is inert on inline boxes)."
      ]
    }
  },
  "registryDependencies": [
    "utils"
  ]
}