# Code Block

> Syntax-lite code block with filename header, line numbers, copy button and in-block horizontal scroll.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/code-block
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/code-block.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/code-block-demo.tsx"
import { CodeBlock } from "@/components/code-block"

const ts = `import { createClient } from "@atelier/sdk"

// Create a client once and reuse it
const client = createClient({ apiKey: process.env.ATELIER_KEY, retries: 3 })

export async function loadProjects(limit: number = 20) {
  const res = await client.projects.list({ limit, includeArchived: false })
  if (!res.ok) throw new Error("Request failed with status " + res.status)
  return res.data.map((project) => ({ id: project.id, name: project.name }))
}`

const json = `{
  "name": "atelier-ui",
  "version": "1.4.0",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "publish": false,
  "license": null
}`

const bash = `# Install and start the dev server
npm install atelier-ui
git clone https://github.com/atelier/ui.git && cd ui
npm run dev -- --port 3000`

export default function CodeBlockDemo() {
  return (
    <div className="grid w-full max-w-xl gap-6">
      <CodeBlock code={ts} language="ts" filename="src/projects.ts" />
      <div className="grid gap-6 sm:grid-cols-2">
        <CodeBlock code={json} language="json" filename="package.json" />
        <CodeBlock code={bash} language="bash" filename="terminal" showLineNumbers={false} />
      </div>
    </div>
  )
}
```

## Source

```tsx title="components/code-block.tsx"
"use client"

import * as React from "react"
import { Check, Copy } from "lucide-react"

import { cn } from "@/lib/utils"

export type CodeLanguage = "js" | "ts" | "json" | "bash"
type TokenType = "keyword" | "string" | "comment" | "number" | "plain"
interface Token {
  type: TokenType
  text: string
}

const JS_KEYWORDS =
  "const|let|var|function|return|if|else|for|while|import|from|export|default|async|await|new|class|extends|true|false|null|undefined|typeof|throw|try|catch|switch|case|break"
const TS_KEYWORDS = `${JS_KEYWORDS}|interface|type|enum|implements|readonly|as|public|private`
const BASH_KEYWORDS = "if|then|else|fi|for|do|done|while|case|esac|function|export|echo|cd|sudo|npm|npx|git|pnpm|yarn|curl"

function pattern(keywords: string, comment: string) {
  // groups: 1 comment, 2 string, 3 number, 4 keyword
  return new RegExp(
    `(${comment})|("(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)|(\\b\\d+(?:\\.\\d+)?\\b)|\\b(${keywords})\\b`,
    "g"
  )
}

const patterns: Record<CodeLanguage, RegExp> = {
  js: pattern(JS_KEYWORDS, "\\/\\/.*"),
  ts: pattern(TS_KEYWORDS, "\\/\\/.*"),
  json: pattern("true|false|null", "(?!)"),
  bash: pattern(BASH_KEYWORDS, "#.*"),
}

/** Splits a single line into highlight tokens. Pure. */
export function tokenizeLine(line: string, language: CodeLanguage): Token[] {
  const re = new RegExp(patterns[language].source, "g")
  const tokens: Token[] = []
  let last = 0
  let match: RegExpExecArray | null
  while ((match = re.exec(line)) !== null) {
    if (match[0] === "") {
      re.lastIndex++
      continue
    }
    if (match.index > last) tokens.push({ type: "plain", text: line.slice(last, match.index) })
    const type: TokenType = match[1] ? "comment" : match[2] ? "string" : match[3] ? "number" : "keyword"
    tokens.push({ type, text: match[0] })
    last = match.index + match[0].length
  }
  if (last < line.length) tokens.push({ type: "plain", text: line.slice(last) })
  return tokens
}

const tokenClass: Record<TokenType, string> = {
  keyword: "text-violet-600 dark:text-violet-400",
  string: "text-emerald-600 dark:text-emerald-400",
  comment: "text-muted-foreground italic",
  number: "text-amber-600 dark:text-amber-400",
  plain: "",
}

export interface CodeBlockProps extends Omit<React.ComponentProps<"div">, "children"> {
  code: string
  language?: CodeLanguage
  filename?: string
  showLineNumbers?: boolean
}

export function CodeBlock({
  code,
  language = "ts",
  filename,
  showLineNumbers = true,
  className,
  ...props
}: CodeBlockProps) {
  const [copied, setCopied] = React.useState(false)
  const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null)

  React.useEffect(() => {
    return () => {
      if (timer.current) clearTimeout(timer.current)
    }
  }, [])

  async function copy() {
    try {
      await navigator.clipboard.writeText(code)
      setCopied(true)
      if (timer.current) clearTimeout(timer.current)
      timer.current = setTimeout(() => setCopied(false), 1500)
    } catch {
      // Clipboard unavailable (insecure context or denied permission); fail silently.
    }
  }

  const lines = code.replace(/\n$/, "").split("\n")

  return (
    <div
      className={cn("w-full min-w-0 overflow-hidden rounded-xl border bg-card text-card-foreground shadow-sm", className)}
      {...props}
    >
      <div className="flex h-10 items-center justify-between gap-3 border-b bg-muted/50 pr-1.5 pl-4">
        <span className="min-w-0 truncate font-mono text-xs text-muted-foreground">{filename ?? language}</span>
        <button
          type="button"
          onClick={copy}
          aria-label={copied ? "Copied" : "Copy code"}
          className="inline-flex h-7 items-center gap-1.5 rounded-md px-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40"
        >
          {copied ? <Check className="size-3.5 text-emerald-500" aria-hidden /> : <Copy className="size-3.5" aria-hidden />}
          <span aria-live="polite">{copied ? "Copied" : "Copy"}</span>
        </button>
      </div>
      <div className="overflow-x-auto" tabIndex={0} role="region" aria-label={filename ? `Code: ${filename}` : "Code"}>
        <pre className="m-0 w-max min-w-full py-3 font-mono text-[13px] leading-6">
          <code>
            {lines.map((line, i) => (
              <span key={i} className="flex px-4">
                {showLineNumbers && (
                  <span aria-hidden className="mr-4 inline-block w-6 shrink-0 text-right text-muted-foreground select-none tabular-nums">
                    {i + 1}
                  </span>
                )}
                <span className="whitespace-pre">
                  {line === "" ? " " : tokenizeLine(line, language).map((t, j) => (
                    <span key={j} className={tokenClass[t.type]}>
                      {t.text}
                    </span>
                  ))}
                </span>
              </span>
            ))}
          </code>
        </pre>
      </div>
    </div>
  )
}
```
