{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block-editor",
  "type": "registry:component",
  "title": "CodeBlockEditor",
  "description": "Editable code surface with Shiki highlighting and token-based light/dark themes.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "https://control-ui.dev/r/button.json",
    "https://control-ui.dev/r/code.json",
    "https://control-ui.dev/r/core.json",
    "https://control-ui.dev/r/scroll-area.json",
    "https://control-ui.dev/r/tooltip.json",
    "https://control-ui.dev/r/use-copy-to-clipboard.json"
  ],
  "files": [
    {
      "path": "src/registry/knob-contracts/button-knobs.ts",
      "target": "@components/control-ui/knob-contracts/button-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/button.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const buttonKnobs = [\n  \"--cui-button-radius\",\n  \"--cui-button-gap\",\n  \"--cui-button-icon\",\n  \"--cui-button-bg\",\n  \"--cui-button-bg-image\",\n  \"--cui-button-foreground\",\n  \"--cui-button-hover-bg\",\n  \"--cui-button-hover-foreground\",\n  \"--cui-button-press-bg\",\n  \"--cui-button-press-scale\",\n  \"--cui-button-active-bg\",\n  \"--cui-button-active-foreground\",\n  \"--cui-button-active-hover-bg\",\n  \"--cui-button-shadow\",\n  \"--cui-button-hover-shadow\",\n  \"--cui-button-press-shadow\",\n  \"--cui-button-active-shadow\",\n] as const;\nexport type ButtonKnobStyle = Partial<Record<(typeof buttonKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/knob-contracts/code-block-editor-knobs.ts",
      "target": "@components/control-ui/knob-contracts/code-block-editor-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/code-block-editor.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const codeBlockEditorKnobs = [\n  \"--cui-code-block-editor-radius\",\n  \"--cui-code-block-editor-background\",\n  \"--cui-code-block-editor-border-color\",\n  \"--cui-code-block-editor-shadow\",\n  \"--cui-code-block-editor-title-foreground\",\n  \"--cui-code-block-editor-code-background\",\n] as const;\nexport type CodeBlockEditorKnobStyle = Partial<Record<(typeof codeBlockEditorKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/knob-contracts/code-knobs.ts",
      "target": "@components/control-ui/knob-contracts/code-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/code.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const codeKnobs = [\n  \"--cui-code-radius\",\n  \"--cui-code-background\",\n  \"--cui-code-border-color\",\n  \"--cui-code-shadow\",\n  \"--cui-code-title-foreground\",\n  \"--cui-code-text-foreground\",\n] as const;\nexport type CodeKnobStyle = Partial<Record<(typeof codeKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/lib/code-block-shiki.ts",
      "target": "@components/control-ui/lib/code-block-shiki.ts",
      "type": "registry:lib",
      "content": "// compat shim — code primitives (Code, CodeDiff, CodeBlockEditor, markdown fences) share one Shiki\n// tokenizer (code-tokens.ts) on CSS-var theme, so syntax colors follow active skin; keeps\n// original CodeBlockEditor* names working so existing callers need no import changes\n\nimport type { CodeToken, CodeTokenLines, CodeTokenStyle } from \"./code-tokens\";\nimport { highlightToTokens, normalizeLanguage as normalizeCodeLanguage } from \"./code-tokens\";\n\nexport type CodeBlockEditorToken = CodeToken;\nexport type CodeBlockEditorTokenLines = CodeTokenLines;\nexport type CodeBlockEditorTokenStyle = CodeTokenStyle;\n\nexport const highlightCodeToTokens = highlightToTokens;\nexport const normalizeLanguage = normalizeCodeLanguage;\n"
    },
    {
      "path": "src/registry/sources/control-ui/code-block-editor.tsx",
      "target": "@components/control-ui/code-block-editor.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon, CopyIcon } from \"lucide-react\";\nimport type { ChangeEvent, ComponentProps, CSSProperties, ReactNode } from \"react\";\nimport { Children, createContext, Fragment, isValidElement, use, useEffect, useState } from \"react\";\nimport { useCopyToClipboard } from \"@/components/control-ui/hooks/use-copy-to-clipboard\";\nimport type { ButtonKnobStyle } from \"@/components/control-ui/knob-contracts/button-knobs\";\nimport type { CodeBlockEditorKnobStyle } from \"@/components/control-ui/knob-contracts/code-block-editor-knobs\";\nimport type { CodeKnobStyle } from \"@/components/control-ui/knob-contracts/code-knobs\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\nimport {\n  type CodeBlockEditorTokenLines,\n  type CodeBlockEditorTokenStyle,\n  highlightCodeToTokens,\n} from \"@/components/control-ui/lib/code-block-shiki\";\nimport { Button } from \"@/components/control-ui/ui/button\";\nimport { ScrollArea } from \"@/components/control-ui/ui/scroll-area\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/control-ui/ui/tooltip\";\n\ntype CodeBlockEditorStyleProps<Props, Style> = Omit<Props, \"style\"> & { style?: CSSProperties & Style };\n\nexport type CodeBlockEditorOverflow = \"wrap\" | \"scroll\";\nexport type CodeBlockEditorHighlight = \"auto\" | \"none\";\nexport type CodeBlockEditorChrome = \"standalone\" | \"embedded\";\nexport type CodeBlockEditorDensity = \"default\" | \"compact\";\nexport type CodeBlockEditorVariant = \"default\" | \"command\";\n\ntype CodeBlockEditorContextValue = {\n  overflow: CodeBlockEditorOverflow;\n  chrome: CodeBlockEditorChrome;\n  density: CodeBlockEditorDensity;\n  variant: CodeBlockEditorVariant;\n  hasHeader: boolean;\n};\n\nconst CodeBlockEditorContext = createContext<CodeBlockEditorContextValue | null>(null);\n\nfunction useCodeBlockEditorContext() {\n  const context = use(CodeBlockEditorContext);\n  if (!context) throw new Error(\"CodeBlockEditor compound parts must be rendered inside <CodeBlockEditor>.\");\n  return context;\n}\n\nfunction resolveCodeBlockEditorClasses({ overflow, density, variant }: CodeBlockEditorContextValue) {\n  const isCommand = variant === \"command\";\n  const isCompact = density === \"compact\" || isCommand;\n  const scrollOverflowClassName = isCommand ? \"w-full max-w-full overflow-x-auto whitespace-pre\" : \"w-max min-w-full whitespace-pre\";\n\n  return {\n    codeClassName: cn(\n      isCompact ? \"min-h-0 px-3 py-2\" : \"min-h-[160px] p-4\",\n      overflow === \"scroll\" ? scrollOverflowClassName : \"w-full whitespace-pre-wrap break-words\",\n    ),\n    editorClassName: cn(\n      \"w-full max-w-full overflow-auto\",\n      isCompact ? \"max-h-[320px] min-h-0 px-3 py-2\" : \"max-h-[520px] min-h-[160px] p-4\",\n      overflow === \"scroll\" ? \"whitespace-pre\" : \"whitespace-pre-wrap break-words\",\n    ),\n    scrollable: !isCommand,\n    maxHeight: isCompact ? \"320px\" : \"520px\",\n  };\n}\n\nexport type CodeBlockEditorProps = CodeBlockEditorStyleProps<ComponentProps<\"figure\">, CodeBlockEditorKnobStyle> & {\n  overflow?: CodeBlockEditorOverflow;\n  chrome?: CodeBlockEditorChrome;\n  density?: CodeBlockEditorDensity;\n  variant?: CodeBlockEditorVariant;\n};\n\nfunction hasCodeBlockEditorHeader(children: ReactNode) {\n  return Children.toArray(children).some((child) => isValidElement(child) && child.type === CodeBlockEditorHeader);\n}\n\nexport type CodeBlockEditorHeaderProps = CodeBlockEditorStyleProps<ComponentProps<\"figcaption\">, CodeBlockEditorKnobStyle>;\nexport type CodeBlockEditorTitleProps = CodeBlockEditorStyleProps<ComponentProps<\"span\">, CodeBlockEditorKnobStyle>;\nexport type CodeBlockEditorActionsProps = ComponentProps<\"div\"> & { style?: CSSProperties & CodeBlockEditorKnobStyle };\n\nexport type CodeBlockEditorCopyProps = Omit<ComponentProps<typeof Button>, \"children\" | \"onClick\"> & {\n  value: string;\n  children?: ReactNode;\n  copiedLabel?: ReactNode;\n  copiedAriaLabel?: string;\n};\n\nexport type CodeBlockEditorContentProps = CodeBlockEditorStyleProps<Omit<ComponentProps<\"pre\">, \"children\">, CodeBlockEditorKnobStyle> & {\n  code: string;\n  lang?: string;\n  tokens?: CodeBlockEditorTokenLines | null;\n  highlight?: CodeBlockEditorHighlight;\n  maxHeight?: string;\n  scrollAreaClassName?: string;\n};\n\nexport type CodeBlockEditorTextareaProps = CodeBlockEditorStyleProps<\n  Omit<ComponentProps<\"textarea\">, \"defaultValue\" | \"value\">,\n  CodeBlockEditorKnobStyle\n> & {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  fileName?: string;\n};\n\nfunction createKeyedTokenLines(lines: CodeBlockEditorTokenLines) {\n  return lines.reduce<{\n    offset: number;\n    lines: { key: number; tokens: { key: number; content: string; style?: CodeBlockEditorTokenStyle }[] }[];\n  }>(\n    (state, line) => {\n      const tokenState = line.reduce<{ offset: number; tokens: { key: number; content: string; style?: CodeBlockEditorTokenStyle }[] }>(\n        (tokenAccumulator, token) => ({\n          offset: tokenAccumulator.offset + token.content.length,\n          tokens: [\n            ...tokenAccumulator.tokens,\n            {\n              key: tokenAccumulator.offset,\n              content: token.content,\n              style: token.style,\n            },\n          ],\n        }),\n        { offset: state.offset, tokens: [] },\n      );\n\n      return {\n        offset: tokenState.offset + 1,\n        lines: [\n          ...state.lines,\n          {\n            key: state.offset,\n            tokens: tokenState.tokens,\n          },\n        ],\n      };\n    },\n    { offset: 0, lines: [] },\n  ).lines;\n}\n\nfunction useHighlightedTokens({\n  code,\n  lang,\n  tokens,\n  highlight,\n}: {\n  code: string;\n  lang?: string;\n  tokens?: CodeBlockEditorTokenLines | null;\n  highlight: CodeBlockEditorHighlight;\n}) {\n  const tokenKey = `${lang ?? \"\"}\\n${code}`;\n  const [clientTokens, setClientTokens] = useState<{ key: string; tokens: CodeBlockEditorTokenLines | null } | null>(null);\n\n  useEffect(() => {\n    if (highlight === \"none\" || tokens !== undefined || !lang) return;\n\n    let cancelled = false;\n    const requestKey = `${lang}\\n${code}`;\n\n    void highlightCodeToTokens(code, lang)\n      .then((result) => {\n        if (!cancelled) setClientTokens({ key: requestKey, tokens: result });\n      })\n      .catch(() => {\n        if (!cancelled) setClientTokens({ key: requestKey, tokens: null });\n      });\n\n    return () => {\n      cancelled = true;\n    };\n  }, [code, lang, tokens, highlight]);\n\n  if (highlight === \"none\") return null;\n  return tokens ?? (clientTokens?.key === tokenKey ? clientTokens.tokens : null);\n}\n\nfunction CodeBlockEditorCode({\n  code,\n  lang,\n  tokens,\n  highlight,\n  maxHeight,\n  scrollAreaClassName,\n  scrollable,\n  className,\n  ...props\n}: ComponentProps<\"pre\"> & {\n  code: string;\n  lang?: string;\n  tokens?: CodeBlockEditorTokenLines | null;\n  highlight: CodeBlockEditorHighlight;\n  maxHeight: string;\n  scrollAreaClassName?: string;\n  scrollable: boolean;\n}) {\n  const highlightedTokens = useHighlightedTokens({ code, lang, tokens, highlight });\n\n  const codeNode =\n    highlightedTokens === null || highlightedTokens.length === 0 ? (\n      <code>{code}</code>\n    ) : (\n      <code>\n        {createKeyedTokenLines(highlightedTokens).map((line, lineIndex, keyedLines) => (\n          <Fragment key={line.key}>\n            <span>\n              {line.tokens.map((token) => (\n                // colour arrives as var(--code-token-*), so it follows active skin with no dual light/dark className\n                <span key={token.key} style={token.style}>\n                  {token.content}\n                </span>\n              ))}\n            </span>\n            {lineIndex !== keyedLines.length - 1 ? \"\\n\" : null}\n          </Fragment>\n        ))}\n      </code>\n    );\n\n  const block = (\n    <pre data-control-ui=\"code-block-editor\" data-control-family=\"code-block-editor\" data-slot=\"content\" className={className} {...props}>\n      {codeNode}\n    </pre>\n  );\n\n  if (!scrollable) return block;\n\n  return (\n    <ScrollArea className={scrollAreaClassName} maxHeight={maxHeight}>\n      {block}\n    </ScrollArea>\n  );\n}\n\nexport function CodeBlockEditor({\n  overflow,\n  chrome = \"standalone\",\n  density = \"default\",\n  variant = \"default\",\n  className,\n  children,\n  ...props\n}: CodeBlockEditorProps) {\n  const isEmbedded = chrome === \"embedded\";\n  const resolvedOverflow = overflow ?? (variant === \"command\" ? \"wrap\" : \"scroll\");\n  const hasHeader = hasCodeBlockEditorHeader(children);\n\n  return (\n    <CodeBlockEditorContext.Provider value={{ overflow: resolvedOverflow, chrome, density, variant, hasHeader }}>\n      <figure\n        data-control-ui=\"code-block-editor\"\n        data-control-family=\"code-block-editor\"\n        data-slot=\"root\"\n        data-surface=\"panel\"\n        data-variant={variant}\n        data-chrome={chrome}\n        data-density={density}\n        data-header={hasHeader ? \"true\" : undefined}\n        className={cn(\"min-w-0\", !hasHeader && \"relative\", isEmbedded ? \"my-0 overflow-hidden\" : \"my-4 overflow-hidden\", className)}\n        {...props}\n      >\n        {children}\n      </figure>\n    </CodeBlockEditorContext.Provider>\n  );\n}\n\nexport function CodeBlockEditorHeader({ className, ...props }: CodeBlockEditorHeaderProps) {\n  return (\n    <figcaption\n      data-control-ui=\"code-block-editor\"\n      data-control-family=\"code-block-editor\"\n      data-slot=\"header\"\n      className={cn(\"flex min-h-11 items-center justify-between gap-3 px-3 py-2\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function CodeBlockEditorTitle({ className, ...props }: CodeBlockEditorTitleProps) {\n  return (\n    <span\n      data-control-ui=\"code-block-editor\"\n      data-control-family=\"code-block-editor\"\n      data-slot=\"title\"\n      className={cn(\"block min-w-0 truncate\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function CodeBlockEditorActions({ className, ...props }: CodeBlockEditorActionsProps) {\n  return (\n    <div\n      data-control-ui=\"code-block-editor\"\n      data-control-family=\"code-block-editor\"\n      data-slot=\"actions\"\n      className={cn(\"flex shrink-0 items-center gap-1\", className)}\n      {...props}\n    />\n  );\n}\n\n// icon-only unless caller passes children — header copy and overlay copy stay one button\nexport function CodeBlockEditorCopy({\n  value,\n  copiedLabel,\n  copiedAriaLabel = \"Copied\",\n  children,\n  className,\n  \"aria-label\": ariaLabel,\n  ...props\n}: CodeBlockEditorCopyProps) {\n  const { isCopied, handleCopy } = useCopyToClipboard({ text: value });\n  const isIconOnly = children === undefined;\n  // text mode already carries its name in label\n  const label = ariaLabel ?? (isIconOnly ? \"Copy code\" : undefined);\n  const copied = copiedLabel ?? (isIconOnly ? <CheckIcon aria-hidden=\"true\" className=\"size-3.5\" /> : \"Copied\");\n\n  const button = (\n    <Button\n      type=\"button\"\n      variant=\"quiet\"\n      size=\"xs\"\n      aria-live=\"polite\"\n      aria-label={label && isCopied ? copiedAriaLabel : label}\n      className={cn(isIconOnly && \"size-7 p-0\", className)}\n      {...props}\n      onClick={handleCopy}\n    >\n      {isCopied ? copied : (children ?? <CopyIcon aria-hidden=\"true\" className=\"size-3.5\" />)}\n    </Button>\n  );\n\n  if (!isIconOnly) return button;\n\n  return (\n    <TooltipProvider delay={0}>\n      <Tooltip>\n        <TooltipTrigger render={button} />\n        <TooltipContent side=\"left\">{label}</TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n\nexport type CodeBlockEditorFloatingCopyProps = Omit<CodeBlockEditorCopyProps, \"children\" | \"copiedLabel\" | \"style\"> & {\n  style?: CSSProperties & ButtonKnobStyle & CodeKnobStyle;\n};\n\nexport function CodeBlockEditorFloatingCopy({ className, ...props }: CodeBlockEditorFloatingCopyProps) {\n  return (\n    <CodeBlockEditorCopy data-code-block-editor-floating-copy=\"true\" className={cn(\"absolute top-2 right-2 z-10\", className)} {...props} />\n  );\n}\n\nexport function CodeBlockEditorContent({\n  code,\n  lang,\n  tokens,\n  highlight = \"auto\",\n  maxHeight,\n  scrollAreaClassName,\n  className,\n  ...props\n}: CodeBlockEditorContentProps) {\n  const context = useCodeBlockEditorContext();\n  const classes = resolveCodeBlockEditorClasses(context);\n\n  const content = (\n    <CodeBlockEditorCode\n      code={code}\n      lang={lang}\n      tokens={tokens}\n      highlight={highlight}\n      maxHeight={maxHeight ?? classes.maxHeight}\n      scrollAreaClassName={scrollAreaClassName}\n      scrollable={classes.scrollable}\n      className={cn(classes.codeClassName, className)}\n      {...props}\n    />\n  );\n\n  if (context.hasHeader) return content;\n\n  // reserves exactly overlay's footprint (top-2 + size-7) so no dead band is left\n  return (\n    <div className={cn(\"relative\", context.density === \"compact\" || context.variant === \"command\" ? \"pt-7\" : \"pt-5\")}>\n      <CodeBlockEditorFloatingCopy value={code} />\n      {content}\n    </div>\n  );\n}\n\nexport function CodeBlockEditorTextarea({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onChange,\n  fileName,\n  className,\n  \"aria-label\": ariaLabel,\n  ...props\n}: CodeBlockEditorTextareaProps) {\n  const { editorClassName } = resolveCodeBlockEditorClasses(useCodeBlockEditorContext());\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const currentValue = value ?? internalValue;\n\n  function handleChange(event: ChangeEvent<HTMLTextAreaElement>) {\n    const nextValue = event.target.value;\n    if (value === undefined) setInternalValue(nextValue);\n    onValueChange?.(nextValue);\n    onChange?.(event);\n  }\n\n  return (\n    <textarea\n      data-control-ui=\"code-block-editor\"\n      data-control-family=\"code-block-editor\"\n      data-slot=\"editor\"\n      value={currentValue}\n      onChange={handleChange}\n      aria-label={ariaLabel ?? (fileName ? `${fileName} code` : \"Code editor\")}\n      spellCheck={false}\n      className={cn(editorClassName, \"block resize-y\", className)}\n      {...props}\n    />\n  );\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/code-block-editor.css",
      "target": "@components/control-ui/styles/recipes/code-block-editor.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"code-block-editor\"][data-slot=\"root\"]) {\n    --cui-code-block-editor-background: var(--background);\n    --cui-code-block-editor-border-color: var(--border);\n    --cui-code-block-editor-radius: min(var(--radius-panel), calc(0.5rem * var(--nest-corner-ratio)));\n    --cui-code-block-editor-shadow: var(--shadow-sm);\n    --cui-code-block-editor-code-background: var(--background);\n    --cui-code-block-editor-title-foreground: var(--muted-foreground);\n    border: 1px solid var(--cui-code-block-editor-border-color);\n    border-radius: var(--cui-code-block-editor-radius);\n    background: var(--cui-code-block-editor-background);\n    box-shadow: var(--cui-code-block-editor-shadow);\n  }\n\n  :where([data-control-family=\"code-block-editor\"][data-slot=\"root\"][data-chrome=\"embedded\"]) {\n    border-width: 0px;\n    border-radius: 0px;\n    background: transparent;\n    box-shadow: none;\n  }\n\n  :where([data-control-family=\"code-block-editor\"][data-slot=\"header\"]) {\n    border-bottom: 1px solid var(--border);\n  }\n\n  :where([data-control-family=\"code-block-editor\"][data-slot=\"title\"]) {\n    color: var(--cui-code-block-editor-title-foreground);\n    font-size: var(--text-label);\n    font-family: var(--font-mono);\n  }\n\n  :where([data-control-family=\"code-block-editor\"]:is([data-slot=\"content\"], [data-slot=\"editor\"])) {\n    background: var(--cui-code-block-editor-code-background);\n    color: var(--code-foreground);\n    font-size: var(--text-label);\n  }\n\n  :where(\n    [data-control-family=\"code-block-editor\"][data-slot=\"root\"][data-chrome=\"embedded\"] :is([data-slot=\"content\"], [data-slot=\"editor\"])\n  ) {\n    background: transparent;\n  }\n\n  :where([data-control-family=\"code-block-editor\"][data-slot=\"editor\"]) {\n    border-width: 0px;\n    outline: none;\n  }\n\n  :where([data-control-family=\"button\"][data-code-block-editor-floating-copy=\"true\"]) {\n    background: oklch(from var(--background) l c h / 0.85);\n    box-shadow:\n      inset 0 0 0 1px var(--border),\n      var(--shadow-sm);\n    backdrop-filter: blur(var(--backdrop-blur-popover));\n  }\n}\n\n@property --cui-code-block-editor-radius {\n  syntax: \"<length>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-code-block-editor-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-code-block-editor-border-color {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-code-block-editor-shadow {\n  syntax: \"*\";\n  inherits: true;\n}\n\n@property --cui-code-block-editor-title-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-code-block-editor-code-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n"
    }
  ],
  "css": {
    "@import \"../components/control-ui/styles/recipes/code-block-editor.css\"": {}
  },
  "meta": {}
}
