{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-editor",
  "type": "registry:ui",
  "title": "Code Editor",
  "description": "CodeEditor — an editable code surface whose visual layer is DIAGNOSTICS, not syntax colour.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "blog",
    "primitive"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/cn.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/code-editor.tsx",
      "target": "components/ui/code-editor.tsx",
      "type": "registry:ui",
      "content": "'use client';\n\nimport * as React from 'react';\n\n// @interlace/code-editor v1.0.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/code-editor\n// What changed since: https://ds.interlace.tools/c/code-editor#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\nimport { cn } from '@/lib/utils';\n\n/**\n * CodeEditor — an editable code surface whose visual layer is\n * DIAGNOSTICS, not syntax colour.\n *\n * ## Why there is no client-side syntax highlighting\n *\n * Every in-browser highlighter is a dependency with a grammar registry\n * (Shiki ships megabytes of them), and the editor exists for one job:\n * let a reader paste code and watch analysis light it up. The finding\n * bars ARE the highlighting. A consumer that wants coloured tokens for\n * READ-ONLY code already has CodeBlock — this component is the other\n * half of that pair, not a replacement.\n *\n * ## The zero-sync layout trick\n *\n * Line-highlight overlays usually die by scroll-sync: a scrolling\n * textarea and an absolutely positioned bar layer drift the moment a\n * frame drops. Here the textarea AUTO-GROWS (`rows` = line count, no\n * vertical scroll exists) and soft wrap is off (`wrap=\"off\"`,\n * horizontal overflow scrolls the box like every code block in this\n * package) — so a bar for line N sits at a fixed offset computed from\n * the line-height, forever. No listeners, no rAF, nothing to drift.\n * The pairing is a CONTRACT: `PAD_Y_PX`/`LINE_HEIGHT_PX` below must\n * match the `py-4`/`leading-6` classes on the textarea, and the test\n * suite pins them together.\n *\n * ## A11y\n *\n * A textarea is natively focusable, editable, and announced; `label`\n * is required because an unnamed editor is a mystery box. The bars are\n * `aria-hidden` POSITION, never information: the consumer (see\n * LintPlayground) renders every finding as text beside the editor —\n * colour-and-position alone never carries the message.\n *\n * | Rule | Concept                    | Where in this file                |\n * | ---- | -------------------------- | --------------------------------- |\n * | R5   | testid required, no default| `'data-testid': string`           |\n * | R6   | data-slot on every part    | `\"code-editor\" / \"-highlights\" / \"-input\"` |\n * | R8   | No isXxx booleans          | (none needed)                     |\n * | R13  | Ecosystem first            | native `<textarea>` IS the editor |\n * | R14  | Controlled + uncontrolled  | `value`/`onValueChange` + `defaultValue` |\n * | R18  | Tailwind; dynamic inline   | bar offsets are computed values   |\n * | R25  | Client component           | editing state                     |\n */\n\n/** One highlighted line. Position only — the message lives in text, beside. */\nexport interface CodeEditorDiagnostic {\n  /** 1-indexed line. Out-of-range lines are simply not drawn. */\n  line: number;\n  severity: 'error' | 'warn';\n}\n\n/**\n * The layout contract with the textarea's classes. `leading-6` = 24px\n * rows, `py-4` = 16px top pad; a bar for line N sits at\n * `PAD_Y_PX + (N-1) * LINE_HEIGHT_PX`. Change either side only with\n * the other.\n */\nexport const LINE_HEIGHT_PX = 24;\nexport const PAD_Y_PX = 16;\n\nconst SEVERITY_BAR: Record<CodeEditorDiagnostic['severity'], string> = {\n  error: 'bg-destructive/15 border-l-2 border-destructive',\n  warn: 'bg-chart-4/15 border-l-2 border-chart-4',\n};\n\nexport interface CodeEditorProps\n  extends Omit<\n    React.ComponentPropsWithoutRef<'textarea'>,\n    'value' | 'defaultValue' | 'onChange' | 'children' | 'wrap' | 'rows'\n  > {\n  /** Stable selector for E2E tests; consumer provides — no default (R5). */\n  'data-testid': string;\n  /** Accessible name. Required: an unnamed editor is a mystery box. */\n  label: string;\n  /** Controlled code. Pair with `onValueChange`. */\n  value?: string;\n  /** Uncontrolled starting code. */\n  defaultValue?: string;\n  onValueChange?: (code: string) => void;\n  /** Lines to light up. Position only — render the messages as text too. */\n  diagnostics?: readonly CodeEditorDiagnostic[];\n  /**\n   * Minimum visible rows, so an empty editor still reads as a place to\n   * type rather than a collapsed input.\n   * @default 4\n   */\n  minRows?: number;\n}\n\nexport const CodeEditor = React.forwardRef<HTMLTextAreaElement, CodeEditorProps>(\n  function CodeEditor(\n    {\n      'data-testid': testId,\n      label,\n      value,\n      defaultValue,\n      onValueChange,\n      diagnostics = [],\n      minRows = 4,\n      className,\n      ...rest\n    },\n    ref,\n  ) {\n    const [uncontrolled, setUncontrolled] = React.useState(defaultValue ?? '');\n    const code = value ?? uncontrolled;\n    const lines = code.split('\\n').length;\n\n    return (\n      <div\n        data-slot=\"code-editor\"\n        data-testid={testId}\n        className={cn(\n          'relative overflow-hidden rounded-lg border border-border bg-card',\n          className,\n        )}\n      >\n        {/* Bars are position, not information: aria-hidden, and every\n            severity also changes the left border, so error/warn stay\n            apart in greyscale. */}\n        <div\n          aria-hidden\n          data-slot=\"code-editor-highlights\"\n          className=\"pointer-events-none absolute inset-0\"\n        >\n          {diagnostics\n            .filter((d) => d.line >= 1 && d.line <= lines)\n            .map((d, index) => (\n              <div\n                key={`${d.line}-${index}`}\n                data-line={d.line}\n                className={cn('absolute inset-x-0', SEVERITY_BAR[d.severity])}\n                style={{\n                  top: PAD_Y_PX + (d.line - 1) * LINE_HEIGHT_PX,\n                  height: LINE_HEIGHT_PX,\n                }}\n              />\n            ))}\n        </div>\n        <textarea\n          ref={ref}\n          data-slot=\"code-editor-input\"\n          aria-label={label}\n          spellCheck={false}\n          autoCapitalize=\"off\"\n          autoCorrect=\"off\"\n          // wrap=\"off\": line N must BE row N for the bars to be honest —\n          // long lines scroll horizontally, the code-block rule.\n          wrap=\"off\"\n          rows={Math.max(minRows, lines)}\n          value={code}\n          onChange={(event) => {\n            if (value === undefined) setUncontrolled(event.target.value);\n            onValueChange?.(event.target.value);\n          }}\n          className={cn(\n            // leading-6 + py-4 are the LINE_HEIGHT_PX / PAD_Y_PX contract.\n            'relative block w-full resize-none overflow-x-auto overflow-y-hidden',\n            'whitespace-pre bg-transparent px-4 py-4 font-mono text-sm leading-6',\n            'text-foreground caret-foreground outline-none',\n            'focus-visible:ring-2 focus-visible:ring-ring',\n          )}\n          {...rest}\n        />\n      </div>\n    );\n  },\n);\n"
    }
  ],
  "meta": {
    "tier": "primitive",
    "client": true,
    "minViewport": null,
    "loading": false,
    "version": "1.0.0",
    "since": null
  },
  "docs": "## @interlace/code-editor\n\nInstalled to `components/ui/code-editor.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/code-editor';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/code-editor\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
