{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "editor-code-editor",
  "type": "registry:component",
  "title": "Code editor",
  "description": "An editable code surface with tokenized highlighting",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "@tanstack/highlight@1.1.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-primitives.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/editor/src/code-editor/code-editor.tsx",
      "target": "@ui/gpuixui/editor/code-editor/code-editor.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { KeyEvent, StyleDesc } from \"@gpuix/react\";\nimport {\n  mergeStyles,\n  resolveStyle,\n  type StateStyle,\n} from \"@gpuix/react/floating\";\nimport {\n  fieldHelp,\n  fieldHint,\n  renderFieldHint,\n} from \"../../primitives/components/hint.js\";\nimport { cn } from \"../../tw/index.js\";\nimport {\n  disabledOpacity,\n  nativeTextTheme,\n  syntaxColors,\n  textStyle,\n} from \"../../tokens/index.js\";\nimport { useTheme } from \"../../tokens/react.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { useState } from \"react\";\nimport { useControllableState } from \"../internal/controllable.js\";\nimport { recipeStyle } from \"../internal/recipe.js\";\nimport { draftOf, labelOf } from \"../internal/text.js\";\nimport { codeLines, type HighlightRun } from \"./highlight.js\";\nimport {\n  enterIndent,\n  tabIndent,\n  tabInserts,\n  tabModeOf,\n  visibleRows,\n  type TabMode,\n} from \"./logic.js\";\n\nconst EDITOR_ROOT =\n  \"relative flex flex-row overflow-hidden rounded-lg border border-input bg-background\";\n\nconst EDITOR_GUTTER =\n  \"flex flex-col items-end bg-muted text-muted-foreground select-none\";\n\nconst EDITOR_CODE = \"relative flex flex-col flex-grow\";\n\nconst MONO_DIGIT = 0.6;\n\nexport interface CodeEditorState {\n  readonly disabled: boolean;\n  readonly focused: boolean;\n}\n\nexport interface CodeEditorProps {\n  readonly value?: string;\n\n  readonly defaultValue?: string;\n\n  readonly onValueChange?: (value: string) => void;\n\n  readonly language?: string;\n\n  readonly lineNumbers?: boolean;\n\n  readonly tabMode?: TabMode;\n\n  readonly indent?: string;\n  readonly placeholder?: string;\n  readonly readOnly?: boolean;\n\n  readonly disabled?: boolean;\n\n  readonly minRows?: number;\n\n  readonly maxRows?: number;\n  readonly fullWidth?: boolean;\n\n  readonly label?: string;\n\n  readonly className?: string;\n\n  readonly style?: StateStyle<CodeEditorState>;\n  readonly testId?: string;\n}\n\nexport const CodeEditor = ({\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  language,\n  lineNumbers = true,\n  tabMode = \"insert\",\n  indent = \"  \",\n  placeholder,\n  readOnly = false,\n  disabled = false,\n  minRows,\n  maxRows,\n  fullWidth = false,\n  label,\n  className,\n  style,\n  testId = \"code-editor\",\n}: CodeEditorProps) => {\n  const theme = useTheme();\n\n  const name = labelOf(label, \"Code editor\");\n  const [value, setValue] = useControllableState({\n    value: valueProp === undefined ? undefined : draftOf(valueProp),\n    defaultValue: draftOf(defaultValue),\n    onChange: onValueChange,\n  });\n  const [focused, setFocused] = useState(false);\n\n  const mode = tabModeOf(tabMode);\n\n  const syntax = syntaxColors(theme);\n  const metrics = textStyle(theme, \"codeBlock\");\n  const lines = codeLines(value, language);\n\n  const body = visibleRows(Arr.length(lines), minRows) - Arr.length(lines);\n  const rows = Arr.appendAll(\n    lines,\n    Arr.take(Arr.replicate(Arr.empty<HighlightRun>(), body), body),\n  );\n  const gutterWidth =\n    Math.ceil(\n      String(Math.max(1, Arr.length(rows))).length *\n        metrics.fontSize *\n        MONO_DIGIT,\n    ) + theme.spacing.sm;\n  const state: CodeEditorState = { disabled, focused };\n\n  const edit = (next: string): void => {\n    if (!readOnly && !disabled) setValue(next);\n  };\n\n  const onChange = (next: string): void =>\n    edit(next === `${value}\\n` ? `${next}${enterIndent(value)}` : next);\n\n  const onKeyDown = (event: KeyEvent): void => {\n    if (readOnly || disabled) return;\n    const indentText = tabIndent(indent);\n    if (event.key === \"tab\" && tabInserts(mode, indentText)) {\n      event.preventDefault();\n      setValue(value + indentText);\n    }\n  };\n\n  const fieldStyle: StyleDesc = {\n    position: \"absolute\",\n    top: 0,\n    right: 0,\n    bottom: 0,\n    left: 0,\n    color: \"transparent\",\n    backgroundColor: \"transparent\",\n    fontSize: metrics.fontSize,\n    lineHeight: metrics.lineHeight,\n    fontFamily: metrics.fontFamily,\n    fontWeight: metrics.fontWeight,\n  };\n\n  const hint = fieldHint({ placeholder, disabled }, theme, value, {\n    origin: \"host\",\n    align: \"top\",\n    style: fieldStyle,\n  });\n  const help = fieldHelp({ placeholder }, value);\n\n  return (\n    <div\n      testId={`${testId}-surface`}\n      style={mergeStyles(\n        mergeStyles(\n          mergeStyles(recipeStyle(cn(EDITOR_ROOT, className), theme), {\n            width: fullWidth ? \"100%\" : undefined,\n          }),\n          disabled ? { opacity: disabledOpacity } : {},\n        ),\n        resolveStyle(style, state),\n      )}\n    >\n      {lineNumbers ? (\n        <div\n          style={mergeStyles(recipeStyle(EDITOR_GUTTER, theme), {\n            width: gutterWidth,\n            flexShrink: 0,\n          })}\n        >\n          {rows.map((_, index) => (\n            <text\n              key={index}\n              role=\"presentation\"\n              style={{\n                ...metrics,\n                color: theme.colors.mutedForeground,\n                textAlign: \"right\",\n                paddingRight: theme.spacing.sm,\n              }}\n            >\n              {String(index + 1)}\n            </text>\n          ))}\n        </div>\n      ) : null}\n      <div style={recipeStyle(EDITOR_CODE, theme)}>\n        <div style={{ display: \"flex\", flexDirection: \"column\" }}>\n          {rows.map((line, index) => (\n            <div\n              key={index}\n              style={{\n                display: \"flex\",\n                flexDirection: \"row\",\n                minHeight: metrics.lineHeight,\n              }}\n            >\n              {line.map((run, runIndex) => (\n                <text\n                  key={runIndex}\n                  role=\"presentation\"\n                  style={{\n                    ...metrics,\n                    color:\n                      run.role === undefined\n                        ? theme.colors.foreground\n                        : syntax[run.role],\n                    whiteSpace: \"nowrap\",\n                  }}\n                >\n                  {run.text}\n                </text>\n              ))}\n            </div>\n          ))}\n        </div>\n        <textarea\n          testId={testId}\n          theme={nativeTextTheme(theme, \"background\")}\n          aria-label={name}\n          {...(help === undefined ? {} : { \"aria-description\": help })}\n          value={value}\n          readOnly={readOnly || disabled}\n          tabIndex={disabled ? -1 : 0}\n          minRows={minRows}\n          maxRows={maxRows}\n          style={fieldStyle}\n          onChange={(event) => onChange(event.value ?? \"\")}\n          onKeyDown={onKeyDown}\n          onFocus={() => setFocused(true)}\n          onBlur={() => setFocused(false)}\n        />\n        {Option.match(hint, {\n          onNone: () => null,\n          onSome: (painted) => renderFieldHint(painted, testId),\n        })}\n      </div>\n    </div>\n  );\n};\n\nexport const codeEditorMeta = {\n  name: \"Code editor\",\n  tier: 4,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"a textarea with transparent ink paints no caret, and it reports no scroll offset (measured; gpuix-capabilities.md §Editable highlighted overlay), so the overlay is a highlight layer over a native field, not a positional editor\",\n    \"Tab insert and Enter auto-indent append at the end of the document — the runtime exposes no caret position to insert at\",\n    \"the two layers align by the codeBlock type scale in one box sized by the highlight layer; pixel-exact alignment is not verified without a native host\",\n    \"maxRows cannot cap the editor's height: the layer cannot follow the field's internal scroll, because the runtime reports no scroll offset\",\n  ],\n  docs: \"docs/code-editor.mdx\",\n  example: \"examples/code-editor.example.tsx\",\n  testFile: \"src/code-editor/highlight.test.ts\",\n} as const;\n"
    },
    {
      "path": "packages/editor/src/code-editor/highlight.ts",
      "target": "@ui/gpuixui/editor/code-editor/highlight.ts",
      "type": "registry:lib",
      "content": "import { tokenize } from \"@tanstack/highlight\";\nimport type { HighlightTokenClass } from \"@tanstack/highlight\";\nimport * as Arr from \"effect/Array\";\nimport type { SyntaxRole } from \"../../tokens/index.js\";\n\nexport interface HighlightRun {\n  readonly text: string;\n\n  readonly role?: SyntaxRole;\n}\n\nexport const tokenRole: Readonly<Record<HighlightTokenClass, SyntaxRole>> = {\n  attr: \"attribute\",\n  \"code-inline\": \"constant\",\n  command: \"functionBuiltin\",\n  comment: \"comment\",\n  deleted: \"invalid\",\n  function: \"function\",\n  heading: \"typeName\",\n  inserted: \"label\",\n  keyword: \"keyword\",\n  link: \"attribute\",\n  literal: \"string\",\n  meta: \"macroName\",\n  number: \"number\",\n  operator: \"operator\",\n  property: \"property\",\n  selector: \"typeName\",\n  string: \"string\",\n  tag: \"tag\",\n  type: \"typeName\",\n  variable: \"variable\",\n};\n\nexport const languageOf = (language: unknown): string =>\n  typeof language === \"string\" ? language : \"\";\n\nexport const codeRuns = (\n  source: string,\n  language: unknown,\n): ReadonlyArray<HighlightRun> =>\n  Arr.map(\n    tokenize(source, { lang: languageOf(language) }).tokens,\n    ({ className, value }) =>\n      className === undefined\n        ? { text: value }\n        : { text: value, role: tokenRole[className] },\n  );\n\nconst LINE_ENDINGS = /\\r\\n?|\\n/;\n\nexport const codeLines = (\n  source: string,\n  language: unknown,\n): ReadonlyArray<ReadonlyArray<HighlightRun>> => {\n  const [, pieces] = Arr.mapAccum(\n    Arr.flatMap(codeRuns(source, language), (run) =>\n      Arr.map(run.text.split(LINE_ENDINGS), (text, index) => ({\n        text,\n        role: run.role,\n        opensRow: index > 0,\n      })),\n    ),\n    0,\n    (row, piece) => {\n      const at = piece.opensRow ? row + 1 : row;\n      return [at, { text: piece.text, role: piece.role, row: at }];\n    },\n  );\n  return Arr.match(pieces, {\n    onEmpty: () => [[]],\n    onNonEmpty: (nonEmpty) =>\n      Arr.map(\n        Arr.groupWith(nonEmpty, (head, piece) => head.row === piece.row),\n        (row) =>\n          Arr.map(\n            Arr.filter(row, (piece) => piece.text.length > 0),\n            (piece) => ({ text: piece.text, role: piece.role }),\n          ),\n      ),\n  });\n};\n"
    },
    {
      "path": "packages/editor/src/code-editor/logic.ts",
      "target": "@ui/gpuixui/editor/code-editor/logic.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as Schema from \"effect/Schema\";\nimport * as Str from \"effect/String\";\n\nexport const TAB_MODES = [\"insert\", \"focus\"] as const;\n\nexport const TabModes = Schema.Literals(TAB_MODES);\n\nexport type TabMode = typeof TabModes.Type;\n\nexport const isTabMode = Schema.is(TabModes);\n\nexport const tabModeOf = (value: unknown): TabMode =>\n  isTabMode(value) ? value : \"insert\";\n\nexport const TAB_INDENT = \"  \";\n\nexport const tabIndent = (indent: unknown): string =>\n  typeof indent === \"string\" ? indent : TAB_INDENT;\n\nexport const tabInserts = (\n  mode: TabMode,\n  indent: string = TAB_INDENT,\n): boolean => mode === \"insert\" && indent.length > 0;\n\nexport const leadingIndent = (line: string): string =>\n  Arr.join(\n    Arr.takeWhile(Str.split(line, \"\"), (c) => c === \" \" || c === \"\\t\"),\n    \"\",\n  );\n\nexport const visibleRows = (documentRows: number, minRows?: number): number =>\n  Math.max(\n    Number.isFinite(documentRows) ? Math.max(1, Math.floor(documentRows)) : 1,\n    minRows !== undefined && Number.isFinite(minRows)\n      ? Math.max(0, Math.floor(minRows))\n      : 0,\n  );\n\nexport const enterIndent = (value: string): string =>\n  Option.match(Arr.last(Str.split(/\\r\\n?|\\n/)(value)), {\n    onNone: () => \"\",\n    onSome: leadingIndent,\n  });\n"
    },
    {
      "path": "packages/editor/src/internal/controllable.ts",
      "target": "@ui/gpuixui/editor/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/editor/src/internal/recipe.ts",
      "target": "@ui/gpuixui/editor/internal/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { classesToStyle } from \"../../tw/index.js\";\nimport type { Theme } from \"../../tokens/index.js\";\n\nexport const recipeStyle = (recipe: string, theme: Theme): StyleDesc =>\n  classesToStyle(recipe, theme).style;\n"
    },
    {
      "path": "packages/editor/src/internal/text.ts",
      "target": "@ui/gpuixui/editor/internal/text.ts",
      "type": "registry:lib",
      "content": "import { accessibleName } from \"../../primitives/core/index.js\";\n\nexport const draftOf = (value: unknown): string =>\n  typeof value === \"string\" ? value : \"\";\n\nexport const labelOf = (label: unknown, fallback: string): string =>\n  accessibleName(draftOf(label)) ?? fallback;\n"
    }
  ],
  "docs": "[Code editor](https://gpuixui.sudokw.com/editor/code-editor) — 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": "/editor/code-editor",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}