{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-inline-edit",
  "type": "registry:component",
  "title": "Inline edit",
  "description": "Rename a value where it is displayed",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/components/src/inline-edit.tsx",
      "target": "@ui/gpuixui/components/inline-edit.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { KeyEvent, InputProps, PublicInstance } from \"@gpuix/react\";\nimport { useGpuix } from \"@gpuix/react\";\nimport {\n  mergeStyles,\n  resolveStyle,\n  type StateStyle,\n} from \"@gpuix/react/floating\";\nimport {\n  cn,\n  INLINE_EDIT_DISPLAY,\n  INLINE_EDIT_DISPLAY_TEXT,\n  INLINE_EDIT_INPUT,\n} from \"../tw/index.js\";\nimport { useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport { useControllableState } from \"./internal/controllable.js\";\nimport { activationHandlers, isDismiss, isEnter } from \"./internal/key.js\";\nimport { recipeStyle } from \"./internal/recipe.js\";\nimport { useMergedRef } from \"./internal/ref.js\";\nimport {\n  controlHeight,\n  keepContent,\n  controlRing,\n  disabledFade,\n  nativeTextTheme,\n  selectionWash,\n  useTheme,\n} from \"./internal/theme.js\";\n\nexport interface InlineEditProps extends Omit<InputProps, \"value\" | \"style\"> {\n  readonly value: string;\n\n  readonly onCommit: (value: string) => void;\n\n  readonly label: string;\n\n  readonly placeholder?: string;\n  readonly disabled?: boolean;\n\n  readonly editing?: boolean;\n  readonly defaultEditing?: boolean;\n  readonly onEditingChange?: (editing: boolean) => void;\n\n  readonly className?: string;\n\n  readonly style?: StateStyle<InlineEditState>;\n}\n\nexport interface InlineEditState {\n  readonly editing: boolean;\n  readonly disabled: boolean;\n}\n\nexport const InlineEdit = ({\n  value,\n  onCommit,\n  label,\n  placeholder = \"—\",\n  disabled = false,\n  editing: editingProp,\n  defaultEditing = false,\n  onEditingChange,\n  className,\n  style,\n  onChange,\n  onKeyDown,\n  onFocus,\n  onBlur,\n  onSubmit: onSubmitProp,\n  ref,\n  ...rest\n}: InlineEditProps) => {\n  const theme = useTheme();\n  const renderer = useGpuix().renderer;\n  const [editing, setEditing] = useControllableState({\n    value: editingProp,\n    defaultValue: defaultEditing,\n    onChange: onEditingChange,\n  });\n  const [draft, setDraft] = useState(value);\n  const [input, setInput] = useState<PublicInstance | undefined>(undefined);\n  const display = useRef<PublicInstance | null>(null);\n\n  const finished = useRef(false);\n\n  const attachInput = useCallback((next: PublicInstance | null) => {\n    setInput(next ?? undefined);\n  }, []);\n  const attachDisplay = useCallback((next: PublicInstance | null) => {\n    display.current = next;\n  }, []);\n  const setDisplay = useMergedRef<PublicInstance | null>(attachDisplay, ref);\n\n  const inputId = input?.id;\n  const refocusDisplay = useRef(false);\n\n  useLayoutEffect(() => {\n    if (inputId === undefined) return;\n    renderer?.focusElement?.(inputId);\n  }, [inputId, renderer]);\n\n  useLayoutEffect(() => {\n    if (editing || !refocusDisplay.current) return;\n    refocusDisplay.current = false;\n    const instance = display.current;\n    if (instance !== null) renderer?.focusElement?.(instance.id);\n  }, [editing, renderer]);\n\n  const start = (): void => {\n    if (disabled || editing) return;\n    finished.current = false;\n    setDraft(value);\n    setEditing(true);\n  };\n\n  const finish = (commit: boolean, refocus: boolean): void => {\n    if (finished.current) return;\n    finished.current = true;\n    refocusDisplay.current = refocus;\n    setEditing(false);\n\n    if (commit && !disabled && draft !== value) onCommit(draft);\n  };\n\n  const cancel = (): void => finish(false, true);\n\n  const commit = (): void => finish(true, true);\n\n  const handleInputKeyDown = (event: KeyEvent): void => {\n    onKeyDown?.(event);\n    if (isDismiss(event)) {\n      cancel();\n    } else if (isEnter(event)) {\n      commit();\n    }\n  };\n\n  const state: InlineEditState = { editing, disabled };\n\n  if (!editing) {\n    return (\n      <div\n        role=\"button\"\n        aria-label={label}\n        tabIndex={disabled ? -1 : 0}\n        {...rest}\n        ref={setDisplay}\n        onClick={start}\n        {...activationHandlers(start)}\n        onFocus={(event) => onFocus?.(event)}\n        onBlur={(event) => onBlur?.(event)}\n        style={mergeStyles(\n          mergeStyles(recipeStyle(cn(INLINE_EDIT_DISPLAY, className), theme), {\n            minHeight: controlHeight(theme, \"sm\"),\n            ...disabledFade(disabled),\n            ...controlRing(theme, { disabled }),\n            hover: { backgroundColor: theme.colors.accent },\n          }),\n          resolveStyle(style, state),\n        )}\n      >\n        <text\n          style={mergeStyles(recipeStyle(INLINE_EDIT_DISPLAY_TEXT, theme), {\n            color:\n              value === \"\"\n                ? theme.colors.mutedForeground\n                : theme.colors.foreground,\n          })}\n        >\n          {value === \"\" ? placeholder : value}\n        </text>\n      </div>\n    );\n  }\n\n  return (\n    <input\n      {...rest}\n      theme={nativeTextTheme(theme, \"background\")}\n      aria-label={label}\n      ref={attachInput}\n      value={draft}\n      readOnly={disabled}\n      tabIndex={disabled ? -1 : 0}\n      onChange={(event) => {\n        setDraft(event.value ?? \"\");\n        onChange?.(event);\n      }}\n      onKeyDown={handleInputKeyDown}\n      onFocus={(event) => onFocus?.(event)}\n      onSubmit={(event) => {\n        commit();\n        onSubmitProp?.(event);\n      }}\n      onBlur={(event) => {\n        finish(true, false);\n        onBlur?.(event);\n      }}\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(INLINE_EDIT_INPUT, className), theme), {\n          height: controlHeight(theme, \"md\"),\n          ...controlRing(theme, { disabled }),\n          ...selectionWash(theme),\n          ...keepContent,\n        }),\n        resolveStyle(style, state),\n      )}\n    />\n  );\n};\n\nexport const inlineEditMeta = {\n  name: \"Inline edit\",\n  tier: 2,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"no contenteditable in 0.11.0: display and input are swapped explicitly\",\n    \"blur commits; Escape cancels; empty text is a valid committed value\",\n    \"a control disabled mid-edit discards the draft instead of committing it\",\n    \"no aria-invalid or aria-describedby, so a rejected commit is the consumer's feedback\",\n  ],\n  docs: \"docs/inline-edit.mdx\",\n  example: \"examples/inline-edit.example.tsx\",\n  testFile: \"src/behaviour/inline-edit.test.tsx\",\n} as const;\n"
    },
    {
      "path": "packages/components/src/internal/controllable.ts",
      "target": "@ui/gpuixui/components/internal/controllable.ts",
      "type": "registry:lib",
      "content": "import { useCallback, useState } from \"react\";\n\nexport type ControllableState<Value> = readonly [Value, (next: Value) => void];\n\nexport interface ControllableStateOptions<Value> {\n  readonly value: Value | undefined;\n\n  readonly defaultValue: Value;\n\n  readonly onChange?: (next: Value) => void;\n}\n\nexport const useControllableState = <Value>({\n  value,\n  defaultValue,\n  onChange,\n}: ControllableStateOptions<Value>): ControllableState<Value> => {\n  const [internal, setInternal] = useState(defaultValue);\n  const controlled = value !== undefined;\n\n  const setValue = useCallback(\n    (next: Value) => {\n      if (!controlled) setInternal(next);\n      if (value !== next) onChange?.(next);\n    },\n    [controlled, value, onChange],\n  );\n\n  return [controlled ? value : internal, setValue];\n};\n"
    },
    {
      "path": "packages/components/src/internal/key.ts",
      "target": "@ui/gpuixui/components/internal/key.ts",
      "type": "registry:lib",
      "content": "import type { EventPayload, KeyEvent, Props } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\n\nexport type KeyPhase = \"down\" | \"up\";\n\nexport const isActivation = (event: EventPayload, phase: KeyPhase): boolean => {\n  if (event.isHeld === true) return false;\n  if (event.key === \"enter\") return phase === \"down\";\n  return event.key === \"space\" && phase === \"up\";\n};\n\nexport const swallowsDefault = (\n  event: EventPayload,\n  phase: KeyPhase,\n): boolean => event.key === \"space\" && phase === \"down\";\n\nexport const activationHandlers = (\n  activate: (event: KeyEvent) => void,\n  observe: Pick<Props, \"onKeyDown\" | \"onKeyUp\"> = {},\n): Pick<Props, \"onKeyDown\" | \"onKeyUp\"> => ({\n  onKeyDown: (event: KeyEvent) => {\n    if (swallowsDefault(event, \"down\")) event.preventDefault();\n    if (isActivation(event, \"down\")) activate(event);\n    observe.onKeyDown?.(event);\n  },\n  onKeyUp: (event: KeyEvent) => {\n    if (isActivation(event, \"up\")) activate(event);\n    observe.onKeyUp?.(event);\n  },\n});\n\nexport const isEnter = (event: EventPayload): boolean => event.key === \"enter\";\n\nexport const isDown = (event: EventPayload): boolean =>\n  event.key === \"down\" || (event.key === \"n\" && event.modifiers?.ctrl === true);\n\nexport const isUp = (event: EventPayload): boolean =>\n  event.key === \"up\" || (event.key === \"p\" && event.modifiers?.ctrl === true);\n\nexport const isRight = (event: EventPayload): boolean => event.key === \"right\";\n\nexport const isLeft = (event: EventPayload): boolean => event.key === \"left\";\n\nexport const isFirst = (event: EventPayload): boolean => event.key === \"home\";\n\nexport const isLast = (event: EventPayload): boolean => event.key === \"end\";\n\nexport const isPageUp = (event: EventPayload): boolean =>\n  event.key === \"pageup\";\n\nexport const isPageDown = (event: EventPayload): boolean =>\n  event.key === \"pagedown\";\n\nexport const isDismiss = (event: EventPayload): boolean =>\n  event.key === \"escape\";\n\nexport const isTab = (event: EventPayload): boolean => event.key === \"tab\";\n\nexport const isSelectAll = (event: EventPayload): boolean =>\n  event.key === \"a\" &&\n  (event.modifiers?.cmd === true || event.modifiers?.ctrl === true);\n\nexport const isMenuKey = (event: EventPayload): boolean =>\n  (event.key === \"f10\" && event.modifiers?.shift === true) ||\n  event.key === \"contextmenu\";\n\nexport const axisDirection = (\n  event: EventPayload,\n  orientation: \"horizontal\" | \"vertical\",\n): Option.Option<1 | -1> => {\n  if (event.key === \"right\") return Option.some(1);\n  if (event.key === \"left\") return Option.some(-1);\n  if (orientation === \"vertical\") {\n    if (isDown(event)) return Option.some(1);\n    if (isUp(event)) return Option.some(-1);\n  }\n  return Option.none();\n};\n"
    },
    {
      "path": "packages/components/src/internal/recipe.ts",
      "target": "@ui/gpuixui/components/internal/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(512);\n"
    },
    {
      "path": "packages/components/src/internal/ref.ts",
      "target": "@ui/gpuixui/components/internal/ref.ts",
      "type": "registry:lib",
      "content": "import type { Ref, RefCallback } from \"react\";\nimport { useCallback } from \"react\";\n\nconst assign = <T>(ref: Ref<T> | undefined, value: T): void => {\n  if (ref === null || ref === undefined) return;\n  if (typeof ref === \"function\") {\n    ref(value);\n    return;\n  }\n  ref.current = value;\n};\n\nexport const useMergedRef = <T>(\n  internal: Ref<T>,\n  caller: Ref<T> | undefined,\n): RefCallback<T> =>\n  useCallback(\n    (value: T) => {\n      assign(internal, value);\n      assign(caller, value);\n    },\n    [internal, caller],\n  );\n"
    },
    {
      "path": "packages/components/src/internal/theme.ts",
      "target": "@ui/gpuixui/components/internal/theme.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport * as Schema from \"effect/Schema\";\nimport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  brandTextOn,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  pointerWash,\n  selectionOn,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n  type BoxShadow,\n  type ElevationLevel,\n  type FindHighlightSource,\n  type NativeTextFill,\n  type PlatformName,\n  type PointerWashSurface,\n  type StatusTone,\n  type SyntaxColors,\n  type SyntaxRole,\n  type TextRole,\n  type Theme,\n  type ThemeColors,\n} from \"../../tokens/index.js\";\n\nexport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n};\nexport type {\n  BoxShadow,\n  ElevationLevel,\n  FindHighlightSource,\n  NativeTextFill,\n  PlatformName,\n  StatusTone,\n  SyntaxColors,\n  SyntaxRole,\n  TextRole,\n  Theme,\n};\nexport { ThemeProvider, useTheme } from \"../../tokens/react.js\";\n\nexport const keyboardRing = (theme: Theme, invalid = false): StyleDesc =>\n  controlFocusRing(theme, { invalid }) ?? {};\n\nexport interface ControlRingState {\n  readonly disabled?: boolean;\n\n  readonly invalid?: boolean;\n}\n\nexport const controlRing = (\n  theme: Theme,\n  state: ControlRingState = {},\n): StyleDesc => controlFocusRing(theme, state) ?? {};\n\nexport const pressWash = (theme: Theme, replaced: PointerWashSurface): string =>\n  pointerWash(theme, \"active\", replaced) ?? theme.colors.active;\n\nexport const selectionWash = (theme: Theme): StyleDesc => ({\n  selectionColor: theme.colors.selection,\n});\n\nexport const keepContent: StyleDesc = { keyboardFocusDim: false };\n\nexport const disabledFade = (\n  disabled: boolean,\n): { readonly opacity: number } | undefined =>\n  disabled ? { opacity: disabledOpacity } : undefined;\n\nconst Tones = Schema.Literals([\"info\", \"success\", \"warning\", \"danger\"]);\n\nexport type Tone = typeof Tones.Type;\n\nexport const toneOf = (value: unknown): Tone =>\n  Schema.is(Tones)(value) ? value : \"info\";\n\nexport interface ToneStyle {\n  readonly fg: string;\n  readonly bg: string;\n  readonly border: string;\n}\n\nconst TOKEN_TONE: Readonly<Record<Tone, StatusTone>> = {\n  info: \"info\",\n  success: \"success\",\n  warning: \"warning\",\n  danger: \"destructive\",\n};\n\nexport const tone = (theme: Theme, value: Tone): ToneStyle => {\n  const { background, border, foreground } = statusTone(\n    theme,\n    TOKEN_TONE[value],\n  );\n  return { bg: background, border, fg: foreground };\n};\n\nconst softRoles: Readonly<Record<Tone, keyof ThemeColors>> = {\n  info: \"infoSoft\",\n  success: \"successSoft\",\n  warning: \"warningSoft\",\n  danger: \"destructiveSoft\",\n};\n\nexport const messageLink = (theme: Theme, value: Tone): string =>\n  brandTextOn(theme, softRoles[value]);\n\nexport const messageSelection = (theme: Theme, value: Tone): string =>\n  selectionOn(theme, softRoles[value]);\n\nexport const toneSurface = (theme: Theme, value: Tone): StyleDesc => {\n  const { bg, border, fg } = tone(theme, value);\n  return {\n    backgroundColor: bg,\n    color: fg,\n    borderWidth: theme.borderWidth.thin,\n    borderColor: border,\n  };\n};\n"
    }
  ],
  "docs": "[Inline edit](https://gpuixui.sudokw.com/components/inline-edit) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/components/inline-edit",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}