{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"code-editor","type":"registry:ui","title":"CodeEditor","description":"Editor surface \u2014 controlled value, language + theme registries, and a diffBase gutter with live change marks (added \/ modified \/ deleted).","package":"fancy-code","dependencies":["@particle-academy\/react-fancy","@particle-academy\/fancy-file-commons"],"registryDependencies":["languages","themes"],"files":[{"path":"components\/fancy\/code-editor\/CodeEditor.context.ts","content":"import { createContext, useContext } from \"react\";\r\nimport type { CodeEditorContextValue } from \".\/CodeEditor.types\";\r\n\r\nexport const CodeEditorContext = createContext<CodeEditorContextValue | null>(null);\r\n\r\nexport function useCodeEditor(): CodeEditorContextValue {\r\n  const ctx = useContext(CodeEditorContext);\r\n  if (!ctx) {\r\n    throw new Error(\"useCodeEditor must be used within a <CodeEditor> component\");\r\n  }\r\n  return ctx;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/code-editor\/CodeEditor.context.ts"},{"path":"components\/fancy\/code-editor\/CodeEditor.tsx","content":"import { useEffect, useMemo, useRef, useState, useCallback } from \"react\";\r\nimport { cn, useControllableState } from \"@particle-academy\/react-fancy\";\r\nimport { annotateLines } from \"@particle-academy\/fancy-file-commons\";\r\nimport { CodeEditorContext } from \".\/CodeEditor.context\";\r\nimport { CodeEditorPanel } from \".\/CodeEditorPanel\";\r\nimport { CodeEditorToolbar } from \".\/CodeEditorToolbar\";\r\nimport { CodeEditorToolbarSeparator } from \".\/CodeEditorToolbarSeparator\";\r\nimport { CodeEditorStatusBar } from \".\/CodeEditorStatusBar\";\r\nimport { useEditorEngine } from \"..\/..\/hooks\/use-editor-engine\";\r\nimport { useDarkMode } from \"..\/..\/hooks\/use-dark-mode\";\r\nimport { getLanguage } from \"..\/..\/languages\";\r\nimport type { CodeEditorProps, CodeEditorContextValue } from \".\/CodeEditor.types\";\r\n\r\nfunction CodeEditorRoot({\r\n  children,\r\n  className,\r\n  value: valueProp,\r\n  defaultValue = \"\",\r\n  onChange,\r\n  language: languageProp = \"javascript\",\r\n  onLanguageChange,\r\n  theme = \"auto\",\r\n  readOnly = false,\r\n  lineNumbers: lineNumbersProp = true,\r\n  wordWrap: wordWrapProp = false,\r\n  tabSize: tabSizeProp = 2,\r\n  placeholder,\r\n  minHeight,\r\n  maxHeight,\r\n  cursorLine,\r\n  cursorColumn,\r\n  diffBase,\r\n}: CodeEditorProps) {\r\n  const [currentValue, setCurrentValue] = useControllableState(valueProp, defaultValue, onChange);\r\n\r\n  \/\/ Diff gutter: per-line change marks of the current value vs the baseline.\r\n  \/\/ Line-level only (no intra-line segmentation), recomputed as the user types.\r\n  const diffAnnotations = useMemo(\r\n    () => (diffBase == null ? null : annotateLines(diffBase, currentValue)),\r\n    [diffBase, currentValue],\r\n  );\r\n\r\n  \/\/ Reactive dark mode for \"auto\" theme\r\n  const isDark = useDarkMode();\r\n  const resolvedTheme = theme === \"auto\" ? (isDark ? \"dark\" : \"light\") : theme;\r\n\r\n  \/\/ Language state (changeable via toolbar selector)\r\n  const [currentLanguage, setCurrentLanguageState] = useState(() => {\r\n    const def = getLanguage(languageProp);\r\n    return def?.name ?? languageProp;\r\n  });\r\n\r\n  const setLanguage = useCallback(\r\n    (lang: string) => {\r\n      const def = getLanguage(lang);\r\n      const resolved = def?.name ?? lang;\r\n      setCurrentLanguageState(resolved);\r\n      onLanguageChange?.(resolved);\r\n    },\r\n    [onLanguageChange],\r\n  );\r\n\r\n  \/\/ Toggle states\r\n  const [showLineNumbers, setShowLineNumbers] = useState(lineNumbersProp);\r\n  const [isWordWrap, setIsWordWrap] = useState(wordWrapProp);\r\n\r\n  \/\/ Cursor position\r\n  const [cursorPosition, setCursorPosition] = useState({ line: 1, col: 1 });\r\n  const [selectionLength, setSelectionLength] = useState(0);\r\n\r\n  const containerRef = useRef<HTMLDivElement>(null);\r\n\r\n  \/\/ Our custom editor engine\r\n  const engineReturn = useEditorEngine({\r\n    containerRef,\r\n    value: currentValue,\r\n    onChange: setCurrentValue,\r\n    language: currentLanguage,\r\n    theme: resolvedTheme,\r\n    readOnly,\r\n    lineNumbers: showLineNumbers,\r\n    wordWrap: isWordWrap,\r\n    tabSize: tabSizeProp,\r\n    placeholder,\r\n    minHeight,\r\n    maxHeight,\r\n    onCursorChange: ({ line, col, selectionLength: sel }) => {\r\n      setCursorPosition({ line, col });\r\n      setSelectionLength(sel);\r\n    },\r\n  });\r\n\r\n  \/\/ Reveal + place the caret when cursorLine\/cursorColumn change (and on mount).\r\n  \/\/ revealLine is stable, so this fires only on target change; it runs after the\r\n  \/\/ engine's value-sync effect, so the textarea is populated and sized first.\r\n  const { revealLine: revealLineImpl } = engineReturn;\r\n  useEffect(() => {\r\n    if (cursorLine == null) return;\r\n    revealLineImpl(cursorLine, cursorColumn ?? 1, { focus: false });\r\n  }, [cursorLine, cursorColumn, revealLineImpl]);\r\n\r\n  const contextValue = useMemo<CodeEditorContextValue>(\r\n    () => ({\r\n      getValue: () => engineReturn.textareaRef.current?.value ?? currentValue,\r\n      getSelection: () => {\r\n        const ta = engineReturn.textareaRef.current;\r\n        if (!ta) return \"\";\r\n        return ta.value.slice(ta.selectionStart, ta.selectionEnd);\r\n      },\r\n      setValue: (v: string) => setCurrentValue(v),\r\n      replaceSelection: (text: string) => {\r\n        const ta = engineReturn.textareaRef.current;\r\n        if (!ta) return;\r\n        const start = ta.selectionStart;\r\n        const end = ta.selectionEnd;\r\n        const before = ta.value.slice(0, start);\r\n        const after = ta.value.slice(end);\r\n        ta.value = before + text + after;\r\n        ta.selectionStart = ta.selectionEnd = start + text.length;\r\n        setCurrentValue(ta.value);\r\n      },\r\n      focus: () => engineReturn.textareaRef.current?.focus(),\r\n      revealLine: (line: number, column?: number) =>\r\n        engineReturn.revealLine(line, column ?? 1, { focus: true }),\r\n      setCursor: ({ line, column }: { line: number; column?: number }) =>\r\n        engineReturn.revealLine(line, column ?? 1, { focus: true }),\r\n      language: currentLanguage,\r\n      setLanguage,\r\n      theme: resolvedTheme,\r\n      readOnly,\r\n      lineNumbers: showLineNumbers,\r\n      wordWrap: isWordWrap,\r\n      tabSize: tabSizeProp,\r\n      toggleWordWrap: () => setIsWordWrap((w) => !w),\r\n      toggleLineNumbers: () => setShowLineNumbers((l) => !l),\r\n      copyToClipboard: async () => {\r\n        const text = engineReturn.textareaRef.current?.value ?? currentValue;\r\n        await navigator.clipboard.writeText(text);\r\n      },\r\n      cursorPosition,\r\n      selectionLength,\r\n      textareaRef: engineReturn.textareaRef,\r\n      placeholder,\r\n      diffStats: diffAnnotations\r\n        ? {\r\n            added: diffAnnotations.added,\r\n            modified: diffAnnotations.modified,\r\n            removed: diffAnnotations.removed,\r\n          }\r\n        : null,\r\n      _diffAnnotations: diffAnnotations,\r\n      _engineReturn: engineReturn,\r\n      _minHeight: minHeight,\r\n      _maxHeight: maxHeight,\r\n    }),\r\n    [engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, diffAnnotations, minHeight, maxHeight],\r\n  );\r\n\r\n  return (\r\n    <CodeEditorContext.Provider value={contextValue}>\r\n      <div\r\n        data-fancy-code-editor=\"\"\r\n        className={cn(\r\n          \"overflow-hidden\",\r\n          className,\r\n        )}\r\n      >\r\n        {children}\r\n      <\/div>\r\n    <\/CodeEditorContext.Provider>\r\n  );\r\n}\r\n\r\nCodeEditorRoot.displayName = \"CodeEditor\";\r\n\r\nconst ToolbarWithSeparator = Object.assign(CodeEditorToolbar, {\r\n  Separator: CodeEditorToolbarSeparator,\r\n});\r\n\r\nexport const CodeEditor = Object.assign(CodeEditorRoot, {\r\n  Toolbar: ToolbarWithSeparator,\r\n  Panel: CodeEditorPanel,\r\n  StatusBar: CodeEditorStatusBar,\r\n});\r\n","type":"registry:ui","target":"components\/fancy\/code-editor\/CodeEditor.tsx"},{"path":"components\/fancy\/code-editor\/CodeEditor.types.ts","content":"import type { ReactNode, RefObject } from \"react\";\r\nimport type { DiffAnnotations } from \"@particle-academy\/fancy-file-commons\";\r\nimport type { UseEditorEngineReturn } from \"..\/..\/hooks\/use-editor-engine\";\r\n\r\nexport interface CodeEditorProps {\r\n  children: ReactNode;\r\n  className?: string;\r\n  \/** Controlled value *\/\r\n  value?: string;\r\n  \/** Initial value (uncontrolled) *\/\r\n  defaultValue?: string;\r\n  \/** Called when the document changes *\/\r\n  onChange?: (value: string) => void;\r\n  \/** Language name or alias (default: \"javascript\") *\/\r\n  language?: string;\r\n  \/** Called when the language changes via the toolbar selector *\/\r\n  onLanguageChange?: (lang: string) => void;\r\n  \/** Theme name: \"light\", \"dark\", \"auto\", or a custom registered name (default: \"auto\") *\/\r\n  theme?: string;\r\n  \/** Prevent editing (default: false) *\/\r\n  readOnly?: boolean;\r\n  \/** Show line numbers (default: true) *\/\r\n  lineNumbers?: boolean;\r\n  \/** Enable word wrap (default: false) *\/\r\n  wordWrap?: boolean;\r\n  \/** Tab size in spaces (default: 2) *\/\r\n  tabSize?: number;\r\n  \/** Placeholder shown when empty *\/\r\n  placeholder?: string;\r\n  \/** Minimum height in px *\/\r\n  minHeight?: number;\r\n  \/** Maximum height in px (scrolls beyond this) *\/\r\n  maxHeight?: number;\r\n  \/**\r\n   * 1-based line to reveal (scroll to + place the caret on). Applied on mount\r\n   * and whenever it (or `cursorColumn`) changes \u2014 so opening a file *at* a line,\r\n   * or re-targeting an already-open file, scrolls the line into view. Does not\r\n   * steal focus; call the imperative `revealLine`\/`setCursor` (or `focus`) for\r\n   * that. Out-of-range values clamp to document bounds.\r\n   *\/\r\n  cursorLine?: number;\r\n  \/** 1-based column for {@link cursorLine} (default 1). *\/\r\n  cursorColumn?: number;\r\n  \/**\r\n   * Baseline document for the diff gutter. When set, the line-number column\r\n   * shows per-line change marks of the CURRENT value against this baseline \u2014\r\n   * VS Code convention: a green bar for added lines, a blue bar for modified\r\n   * lines, and a red wedge where lines were deleted. Recomputed live as the\r\n   * user types (line-level only \u2014 cheap). `null`\/omitted disables the gutter\r\n   * marks. Requires `lineNumbers`.\r\n   *\/\r\n  diffBase?: string | null;\r\n}\r\n\r\nexport interface CodeEditorContextValue {\r\n  \/** Get the current document text *\/\r\n  getValue: () => string;\r\n  \/** Get the currently selected text *\/\r\n  getSelection: () => string;\r\n  \/** Replace the entire document *\/\r\n  setValue: (value: string) => void;\r\n  \/** Replace the current selection *\/\r\n  replaceSelection: (text: string) => void;\r\n  \/** Focus the editor *\/\r\n  focus: () => void;\r\n  \/**\r\n   * Scroll to + place the caret on a 1-based `line` (optional 1-based `column`),\r\n   * focusing the editor. Out-of-range positions clamp to document bounds.\r\n   *\/\r\n  revealLine: (line: number, column?: number) => void;\r\n  \/** Place the caret at a 1-based `{ line, column }` and reveal it (focuses). *\/\r\n  setCursor: (pos: { line: number; column?: number }) => void;\r\n  \/** Current language name *\/\r\n  language: string;\r\n  \/** Change the active language *\/\r\n  setLanguage: (lang: string) => void;\r\n  \/** Current theme name *\/\r\n  theme: string;\r\n  \/** Whether the editor is read-only *\/\r\n  readOnly: boolean;\r\n  \/** Whether line numbers are shown *\/\r\n  lineNumbers: boolean;\r\n  \/** Whether word wrap is enabled *\/\r\n  wordWrap: boolean;\r\n  \/** Current tab size *\/\r\n  tabSize: number;\r\n  \/** Toggle word wrap on\/off *\/\r\n  toggleWordWrap: () => void;\r\n  \/** Toggle line numbers on\/off *\/\r\n  toggleLineNumbers: () => void;\r\n  \/** Copy entire document to clipboard *\/\r\n  copyToClipboard: () => Promise<void>;\r\n  \/** Current cursor position *\/\r\n  cursorPosition: { line: number; col: number };\r\n  \/** Length of the current selection (0 if none) *\/\r\n  selectionLength: number;\r\n  \/**\r\n   * Ref to the underlying `<textarea>` element. Exposed so external\r\n   * widgets \u2014 e.g. `<InputTag>`'s `textareaAdapter` \u2014 can attach to\r\n   * the same input the editor renders into. Will be `null` before\r\n   * the editor has mounted.\r\n   *\/\r\n  textareaRef: RefObject<HTMLTextAreaElement | null>;\r\n  \/** Placeholder text *\/\r\n  placeholder?: string;\r\n  \/**\r\n   * Totals of the live diff against {@link CodeEditorProps.diffBase} \u2014\r\n   * `{ added, modified, removed }` \u2014 or `null` when no baseline is set.\r\n   * Handy for a status-bar chip or a host's save indicator.\r\n   *\/\r\n  diffStats: { added: number; modified: number; removed: number } | null;\r\n  \/** @internal \u2014 per-line gutter annotations for the diff marks. *\/\r\n  _diffAnnotations: DiffAnnotations | null;\r\n  \/** @internal *\/\r\n  _engineReturn: UseEditorEngineReturn | null;\r\n  \/** @internal *\/\r\n  _minHeight?: number;\r\n  \/** @internal *\/\r\n  _maxHeight?: number;\r\n}\r\n\r\nexport interface CodeEditorToolbarProps {\r\n  children?: ReactNode;\r\n  className?: string;\r\n}\r\n\r\nexport interface CodeEditorPanelProps {\r\n  className?: string;\r\n}\r\n\r\nexport interface CodeEditorStatusBarProps {\r\n  children?: ReactNode;\r\n  className?: string;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/code-editor\/CodeEditor.types.ts"},{"path":"components\/fancy\/code-editor\/CodeEditorPanel.tsx","content":"import { useMemo } from \"react\";\r\nimport { cn } from \"@particle-academy\/react-fancy\";\r\nimport { useCodeEditor } from \".\/CodeEditor.context\";\r\nimport {\r\n  diffMarkColor,\r\n  diffRemovedColor,\r\n  gutterDiffMark,\r\n  hasDeletedAtEnd,\r\n} from \".\/diff-gutter\";\r\nimport type { CodeEditorPanelProps } from \".\/CodeEditor.types\";\r\n\r\nexport function CodeEditorPanel({ className }: CodeEditorPanelProps) {\r\n  const {\r\n    _engineReturn: engine,\r\n    lineNumbers,\r\n    wordWrap,\r\n    readOnly,\r\n    placeholder,\r\n    _diffAnnotations: diffAnnotations,\r\n    _maxHeight,\r\n    _minHeight,\r\n  } = useCodeEditor();\r\n\r\n  if (!engine) return null;\r\n\r\n  const {\r\n    textareaRef,\r\n    highlightedHtml,\r\n    lineCount,\r\n    activeLine,\r\n    themeColors,\r\n    handleKeyDown,\r\n    handleInput,\r\n    handleScroll,\r\n    handleSelect,\r\n    scrollTop,\r\n    scrollLeft,\r\n  } = engine;\r\n\r\n  const gutterWidth = lineNumbers ? `${Math.max(String(lineCount).length, 2) * 0.75 + 1.5}em` : \"0\";\r\n\r\n  const lineNumberElements = useMemo(() => {\r\n    if (!lineNumbers) return null;\r\n    const removed = diffRemovedColor(themeColors);\r\n    const lines = [];\r\n    for (let i = 1; i <= lineCount; i++) {\r\n      const mark = gutterDiffMark(diffAnnotations, i);\r\n      lines.push(\r\n        <div\r\n          key={i}\r\n          data-diff={mark?.type}\r\n          data-diff-deleted-above={mark?.deletedAbove ? \"\" : undefined}\r\n          style={{\r\n            position: \"relative\",\r\n            color: i === activeLine ? themeColors.foreground : themeColors.gutterForeground,\r\n            backgroundColor: i === activeLine ? themeColors.activeLineBackground : undefined,\r\n          }}\r\n          className=\"pr-3 text-right text-[13px] leading-[1.5] select-none\"\r\n        >\r\n          {\/* Change bar \u2014 VS Code convention: green added, blue modified. *\/}\r\n          {mark?.type && (\r\n            <span\r\n              aria-hidden=\"true\"\r\n              style={{\r\n                position: \"absolute\",\r\n                left: 0,\r\n                top: 0,\r\n                bottom: 0,\r\n                width: 3,\r\n                borderRadius: 2,\r\n                backgroundColor: diffMarkColor(mark.type, themeColors),\r\n              }}\r\n            \/>\r\n          )}\r\n          {\/* Deletion wedge \u2014 lines were removed immediately above this one. *\/}\r\n          {mark?.deletedAbove && (\r\n            <span\r\n              aria-hidden=\"true\"\r\n              style={{\r\n                position: \"absolute\",\r\n                left: 0,\r\n                top: -4,\r\n                width: 0,\r\n                height: 0,\r\n                borderLeft: `8px solid ${removed}`,\r\n                borderTop: \"4px solid transparent\",\r\n                borderBottom: \"4px solid transparent\",\r\n              }}\r\n            \/>\r\n          )}\r\n          {i}\r\n          {\/* Deletion at EOF \u2014 removed lines hang below the last line. *\/}\r\n          {i === lineCount && hasDeletedAtEnd(diffAnnotations) && (\r\n            <span\r\n              aria-hidden=\"true\"\r\n              data-diff-deleted-at-end=\"\"\r\n              style={{\r\n                position: \"absolute\",\r\n                left: 0,\r\n                bottom: -4,\r\n                width: 0,\r\n                height: 0,\r\n                borderLeft: `8px solid ${removed}`,\r\n                borderTop: \"4px solid transparent\",\r\n                borderBottom: \"4px solid transparent\",\r\n              }}\r\n            \/>\r\n          )}\r\n        <\/div>,\r\n      );\r\n    }\r\n    return lines;\r\n  }, [lineNumbers, lineCount, activeLine, themeColors, diffAnnotations]);\r\n\r\n  const containerStyle: React.CSSProperties = {\r\n    backgroundColor: themeColors.background,\r\n    color: themeColors.foreground,\r\n    minHeight: _minHeight,\r\n    maxHeight: _maxHeight,\r\n  };\r\n\r\n  const isEmpty = textareaRef.current ? textareaRef.current.value.length === 0 : highlightedHtml.length === 0;\r\n\r\n  return (\r\n    <div\r\n      data-fancy-code-panel=\"\"\r\n      className={cn(\"relative overflow-auto\", className)}\r\n      style={containerStyle}\r\n    >\r\n      <div className=\"flex min-h-full\">\r\n        {\/* Line numbers gutter *\/}\r\n        {lineNumbers && (\r\n          <div\r\n            className=\"sticky left-0 z-10 shrink-0 pt-2.5 pb-2.5 pl-2\"\r\n            style={{\r\n              backgroundColor: themeColors.gutterBackground,\r\n              borderRight: `1px solid ${themeColors.gutterBorder}`,\r\n              width: gutterWidth,\r\n            }}\r\n          >\r\n            {lineNumberElements}\r\n          <\/div>\r\n        )}\r\n\r\n        {\/* Editor area *\/}\r\n        <div className={cn(\"relative flex-1\", wordWrap && \"min-w-0\")}>\r\n          {\/* Highlighted code overlay *\/}\r\n          <pre\r\n            className=\"pointer-events-none absolute inset-0 m-0 overflow-hidden border-none p-2.5 text-[13px] leading-[1.5]\"\r\n            aria-hidden=\"true\"\r\n            style={{\r\n              whiteSpace: wordWrap ? \"pre-wrap\" : \"pre\",\r\n              overflowWrap: wordWrap ? \"break-word\" : \"normal\",\r\n            }}\r\n          >\r\n            <code dangerouslySetInnerHTML={{ __html: highlightedHtml + \"\\n\" }} \/>\r\n          <\/pre>\r\n\r\n          {\/* Placeholder *\/}\r\n          {placeholder && isEmpty && (\r\n            <div\r\n              className=\"pointer-events-none absolute left-0 top-0 p-2.5 text-[13px] leading-[1.5] opacity-40\"\r\n              style={{ color: themeColors.foreground }}\r\n            >\r\n              {placeholder}\r\n            <\/div>\r\n          )}\r\n\r\n          {\/* Actual textarea for input *\/}\r\n          <textarea\r\n            ref={textareaRef}\r\n            className={cn(\r\n              \"relative m-0 block resize-none border-none bg-transparent p-2.5 text-[13px] leading-[1.5] text-transparent outline-none\",\r\n              wordWrap ? \"w-full\" : \"min-w-full\",\r\n            )}\r\n            style={{\r\n              caretColor: themeColors.cursorColor,\r\n              minHeight: _minHeight ? _minHeight - 40 : 80,\r\n              overflow: \"hidden\",\r\n              whiteSpace: wordWrap ? \"pre-wrap\" : \"pre\",\r\n              overflowWrap: wordWrap ? \"break-word\" : \"normal\",\r\n            }}\r\n            spellCheck={false}\r\n            autoComplete=\"off\"\r\n            autoCorrect=\"off\"\r\n            autoCapitalize=\"off\"\r\n            readOnly={readOnly}\r\n            onKeyDown={handleKeyDown}\r\n            onInput={handleInput}\r\n            onScroll={handleScroll}\r\n            onSelect={handleSelect}\r\n            onClick={handleSelect}\r\n          \/>\r\n        <\/div>\r\n      <\/div>\r\n    <\/div>\r\n  );\r\n}\r\n\r\nCodeEditorPanel.displayName = \"CodeEditorPanel\";\r\n","type":"registry:ui","target":"components\/fancy\/code-editor\/CodeEditorPanel.tsx"},{"path":"components\/fancy\/code-editor\/CodeEditorStatusBar.tsx","content":"import { cn } from \"@particle-academy\/react-fancy\";\r\nimport { useCodeEditor } from \".\/CodeEditor.context\";\r\nimport type { CodeEditorStatusBarProps } from \".\/CodeEditor.types\";\r\n\r\nexport function CodeEditorStatusBar({ children, className }: CodeEditorStatusBarProps) {\r\n  const { cursorPosition, selectionLength, language, tabSize } = useCodeEditor();\r\n\r\n  return (\r\n    <div\r\n      data-fancy-code-statusbar=\"\"\r\n      className={cn(\r\n        \"flex items-center gap-3 border-t border-zinc-200 px-3 py-1 text-[11px] text-zinc-500 dark:border-zinc-700 dark:text-zinc-400\",\r\n        className,\r\n      )}\r\n    >\r\n      {children ?? (\r\n        <>\r\n          <span>Ln {cursorPosition.line}, Col {cursorPosition.col}<\/span>\r\n          {selectionLength > 0 && <span>{selectionLength} selected<\/span>}\r\n          <span className=\"ml-auto\">{language}<\/span>\r\n          <span>{tabSize} spaces<\/span>\r\n        <\/>\r\n      )}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nCodeEditorStatusBar.displayName = \"CodeEditorStatusBar\";\r\n","type":"registry:ui","target":"components\/fancy\/code-editor\/CodeEditorStatusBar.tsx"},{"path":"components\/fancy\/code-editor\/CodeEditorToolbar.tsx","content":"import { useState } from \"react\";\r\nimport { cn } from \"@particle-academy\/react-fancy\";\r\nimport { useCodeEditor } from \".\/CodeEditor.context\";\r\nimport { CodeEditorToolbarSeparator } from \".\/CodeEditorToolbarSeparator\";\r\nimport { getRegisteredLanguages } from \"..\/..\/languages\";\r\nimport type { CodeEditorToolbarProps } from \".\/CodeEditor.types\";\r\n\r\nconst iconBtnClass =\r\n  \"inline-flex items-center justify-center rounded-md p-1 text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\";\r\n\r\nfunction LanguageSelector() {\r\n  const { language, setLanguage } = useCodeEditor();\r\n  const languages = getRegisteredLanguages();\r\n\r\n  return (\r\n    <select\r\n      value={language}\r\n      onChange={(e) => setLanguage(e.target.value)}\r\n      className=\"h-6 rounded border border-zinc-200 bg-transparent px-1.5 text-[11px] text-zinc-600 outline-none transition-colors hover:border-zinc-300 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-600\"\r\n    >\r\n      {languages.map((lang) => (\r\n        <option key={lang} value={lang}>\r\n          {lang}\r\n        <\/option>\r\n      ))}\r\n    <\/select>\r\n  );\r\n}\r\n\r\nfunction DefaultToolbarActions() {\r\n  const { copyToClipboard, toggleWordWrap, wordWrap } = useCodeEditor();\r\n  const [copied, setCopied] = useState(false);\r\n\r\n  const handleCopy = async () => {\r\n    await copyToClipboard();\r\n    setCopied(true);\r\n    setTimeout(() => setCopied(false), 1500);\r\n  };\r\n\r\n  return (\r\n    <>\r\n      <LanguageSelector \/>\r\n      <CodeEditorToolbarSeparator \/>\r\n      <button\r\n        type=\"button\"\r\n        onClick={toggleWordWrap}\r\n        title={wordWrap ? \"Disable Word Wrap\" : \"Enable Word Wrap\"}\r\n        className={cn(iconBtnClass, wordWrap && \"text-blue-500 dark:text-blue-400\")}\r\n      >\r\n        <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\r\n          <line x1=\"3\" y1=\"6\" x2=\"21\" y2=\"6\" \/>\r\n          <path d=\"M3 12h15a3 3 0 110 6h-4\" \/>\r\n          <polyline points=\"16 16 14 18 16 20\" \/>\r\n          <line x1=\"3\" y1=\"18\" x2=\"10\" y2=\"18\" \/>\r\n        <\/svg>\r\n      <\/button>\r\n      <button type=\"button\" onClick={handleCopy} title=\"Copy to Clipboard\" className={iconBtnClass}>\r\n        {copied ? (\r\n          <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\r\n            <polyline points=\"20 6 9 17 4 12\" \/>\r\n          <\/svg>\r\n        ) : (\r\n          <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\r\n            <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\" \/>\r\n            <path d=\"M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1\" \/>\r\n          <\/svg>\r\n        )}\r\n      <\/button>\r\n    <\/>\r\n  );\r\n}\r\n\r\nexport function CodeEditorToolbar({ children, className }: CodeEditorToolbarProps) {\r\n  const hasChildren = children != null;\r\n\r\n  return (\r\n    <div\r\n      data-fancy-code-toolbar=\"\"\r\n      className={cn(\r\n        \"flex items-center gap-0.5 border-b border-zinc-200 px-2 py-1 dark:border-zinc-700\",\r\n        className,\r\n      )}\r\n    >\r\n      {hasChildren ? children : <DefaultToolbarActions \/>}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nCodeEditorToolbar.displayName = \"CodeEditorToolbar\";\r\n","type":"registry:ui","target":"components\/fancy\/code-editor\/CodeEditorToolbar.tsx"},{"path":"components\/fancy\/code-editor\/CodeEditorToolbarSeparator.tsx","content":"export function CodeEditorToolbarSeparator() {\r\n  return (\r\n    <div\r\n      data-fancy-code-toolbar-separator=\"\"\r\n      className=\"mx-1 h-4 w-px bg-zinc-200 dark:bg-zinc-700\"\r\n    \/>\r\n  );\r\n}\r\n\r\nCodeEditorToolbarSeparator.displayName = \"CodeEditorToolbarSeparator\";\r\n","type":"registry:ui","target":"components\/fancy\/code-editor\/CodeEditorToolbarSeparator.tsx"},{"path":"components\/fancy\/code-editor\/diff-gutter.ts","content":"\/**\n * Diff-gutter model \u2014 the pure mapping between fancy-file-commons line\n * annotations and what the CodeEditor gutter renders per line-number row.\n * Kept DOM-free (like engine\/position.ts) so it's unit-testable; the panel\n * only paints what this resolves.\n *\/\nimport type { DiffAnnotations, LineChangeType } from \"@particle-academy\/fancy-file-commons\";\nimport type { ThemeColors } from \"..\/..\/themes\/types\";\n\n\/** What one line-number row shows: its own change bar and\/or a deletion wedge above. *\/\nexport interface GutterDiffMark {\n  \/** The line's own change (colored bar), if any. *\/\n  type?: LineChangeType;\n  \/** True when one or more lines were deleted immediately above this line (wedge). *\/\n  deletedAbove: boolean;\n}\n\n\/** Resolve the mark for a 1-based line, or null when the line is untouched. *\/\nexport function gutterDiffMark(\n  annotations: DiffAnnotations | null,\n  line: number,\n): GutterDiffMark | null {\n  if (!annotations) return null;\n  const a = annotations.byLine[line];\n  if (!a) return null;\n  return { type: a.type, deletedAbove: (a.deletedAbove ?? 0) > 0 };\n}\n\n\/** Whether the deleted-at-EOF wedge should render under the last line. *\/\nexport function hasDeletedAtEnd(annotations: DiffAnnotations | null): boolean {\n  return (annotations?.deletedAtEnd ?? 0) > 0;\n}\n\n\/** VS Code-convention fallbacks for themes that don't define diff colors. *\/\nconst FALLBACK = { added: \"#3fb950\", modified: \"#4184e4\", removed: \"#f85149\" } as const;\n\n\/** The bar color for a change type, honoring theme overrides. *\/\nexport function diffMarkColor(type: LineChangeType, colors: ThemeColors): string {\n  return type === \"added\"\n    ? (colors.diffAdded ?? FALLBACK.added)\n    : (colors.diffModified ?? FALLBACK.modified);\n}\n\n\/** The deletion-wedge color, honoring theme overrides. *\/\nexport function diffRemovedColor(colors: ThemeColors): string {\n  return colors.diffRemoved ?? FALLBACK.removed;\n}\n","type":"registry:ui","target":"components\/fancy\/code-editor\/diff-gutter.ts"},{"path":"components\/fancy\/code-editor\/index.ts","content":"export { CodeEditor } from \".\/CodeEditor\";\r\nexport { useCodeEditor } from \".\/CodeEditor.context\";\r\nexport type {\r\n  CodeEditorProps,\r\n  CodeEditorContextValue,\r\n  CodeEditorToolbarProps,\r\n  CodeEditorPanelProps,\r\n  CodeEditorStatusBarProps,\r\n} from \".\/CodeEditor.types\";\r\n","type":"registry:ui","target":"components\/fancy\/code-editor\/index.ts"}]}