shadimage
Delta Components
uicodesource: ok · refreshed

Code

Syntax-highlighted code with a copy button, a filename bar, and a package-manager tab strip.

npx shadcn add "https://deltacomponents.dev/r/code.json"
free

Visual style

terminal-mono

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
prism-react-rendererprismjs

Registry dependencies

https://www.deltacomponents.dev/r/utils.jsonhttps://www.deltacomponents.dev/r/button.jsonhttps://www.deltacomponents.dev/r/tokens.jsonhttps://www.deltacomponents.dev/r/clipboard.json

Files3 · source cached 10/7/2026, 3:18:49 PM

registry/ui/code.tsx
"use client"

import * as React from "react"
import { useEffect, useState } from "react"
import { Highlight } from "prism-react-renderer"
import type { PrismTheme } from "prism-react-renderer"

// Side-effect import: registers extra Prism grammars (bash, docker,
// java, ruby, php, …) on the shared instance. Must come before any
// ``<Highlight>`` usage in this module.
import "@/lib/prism-languages"

import { cn } from "@/lib/utils"
import { copyToClipboard } from "@/lib/clipboard"
import { getIconForFile } from "@/components/ui/code-icons"
import { Button } from "@/components/ui/button"

type PackageManager = "npm" | "yarn" | "pnpm" | "bun"

/** Every group the two built-in Prism palettes used to carry as hardcoded hex,
 *  now a named slot in the CSS palette (`--code-token-<key>`, declared in
 *  globals.css §8). One list drives two things: the built-in theme below
 *  (each group's color is just `var(--code-token-<key>)`, resolved by the
 *  `.dark` class with no JS), and `resolveGroupColor` — which walks a
 *  consumer-supplied `adaptiveTheme` palette to find the color it assigns
 *  each group, so that palette can be resolved the same CSS-only way. */
const tokenGroups: { key: string; types: string[]; fontStyle?: "italic" }[] = [
  { key: "comment", types: ["comment"], fontStyle: "italic" },
  { key: "keyword", types: ["keyword"] },
  { key: "property", types: ["property", "property-access", "attr-name"] },
  { key: "tag", types: ["tag"] },
  { key: "punctuation", types: ["punctuation", "symbol", "dom", "operator"] },
  { key: "definition", types: ["definition"] },
  { key: "function", types: ["function"] },
  { key: "string", types: ["string", "char", "attr-value"] },
  { key: "static", types: ["static", "number"] },
  { key: "variable", types: ["variable", "parameter"] },
  { key: "builtin", types: ["builtin", "function-definition"] },
  { key: "class-name", types: ["class-name"] },
]

/** The built-in syntax palette. Every color is a CSS variable reference —
 *  light values on `:root`, dark overrides on `.dark` (globals.css §8) — so
 *  the right palette is already in effect before this ever renders. Merging
 *  the old separate dark/light `PrismTheme`s into one is what makes that
 *  possible: previously the choice of *which* theme object to use lived in
 *  React state, guaranteeing a light-palette frame on every mount. */
const builtinTheme: PrismTheme = {
  plain: { color: "var(--code-fg)", backgroundColor: "var(--code-bg)" },
  styles: tokenGroups.map(({ key, types, fontStyle }) => ({
    types,
    style: {
      color: `var(--code-token-${key})`,
      ...(fontStyle ? { fontStyle } : {}),
    },
  })),
}

/** The color a `PrismTheme` assigns a token group, mirroring how
 *  prism-react-renderer resolves overlapping style entries: the last one
 *  naming the type wins. Falls back to the theme's own plain color for a
 *  group it doesn't style at all — used to project a consumer's
 *  `adaptiveTheme` onto the same `--code-token-*` slots the built-in palette
 *  uses. */
function resolveGroupColor(theme: PrismTheme, types: string[]): string {
  for (let i = theme.styles.length - 1; i >= 0; i--) {
    const entry = theme.styles[i]
    if (types.some((t) => entry.types.includes(t)))
      return entry.style.color ?? theme.plain?.color ?? ""
  }
  return theme.plain?.color ?? ""
}

interface CodeProps extends Omit<React.ComponentProps<"div">, "children"> {
  npm?: string
  yarn?: string
  pnpm?: string
  bun?: string
  defaultPackageManager?: PackageManager
  code?: string
  css?: string
  language?: string
  defaultLanguage?: string
  filename?: string
  showLineNumbers?: boolean
  theme?: PrismTheme
  adaptiveTheme?: {
    light: PrismTheme
    dark: PrismTheme
  }
  /** Paint the header and code surface from the Prism theme's own background
   *  instead of the page's card token. Defaults to true whenever `theme` or
   *  `adaptiveTheme` is supplied — a palette brought from elsewhere carries its
   *  own ground, and pairing its colours with the page's card is what makes a
   *  dark theme read washed out on a light page. */
  useThemeBackground?: boolean
  scrollbar?: boolean
  expandable?: boolean
  defaultExpanded?: boolean
  collapsedHeight?: string
  /** Label on the affordance that opens a collapsed block. Docs previews say
   *  "View code"; a bare code block says "Expand". Expanding is one-way —
   *  there is no collapse control, so a reader never has to re-open what they
   *  just opened. A page load starts collapsed again. */
  expandLabel?: string
}

/** The code's own type size, set on the block's ROOT rather than on the
 *  `<pre>`, so every part of the block inherits it — the highlighted source,
 *  the line-number gutter, the filename bar and the package-manager tabs. That
 *  is what makes a single `text-sm` in `className` resize the whole block:
 *  tailwind-merge drops this default in favour of the caller's utility, where
 *  a size declared further down would have won on specificity and left the
 *  parts at different sizes. */
const CODE_TEXT_SIZE = "text-[14px]"

const monoFontFamily =
  'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'

function parseMarkdownCodeBlock(input: string): {
  code: string
  language: string | null
} {
  const markdownRegex = /^```(\w+)?\n?([\s\S]*?)```$/
  const match = input.trim().match(markdownRegex)

  if (match) {
    return {
      language: match[1] || null,
      code: match[2]?.trim() || "",
    }
  }

  return { code: input, language: null }
}

const languageAliases: Record<string, string> = {
  py: "python",
  js: "javascript",
  ts: "typescript",
  tsx: "tsx",
  jsx: "jsx",
  rb: "ruby",
  rs: "rust",
  sh: "bash",
  shell: "bash",
  zsh: "bash",
  console: "bash",
  yml: "yaml",
  md: "markdown",
  dockerfile: "docker",
  Dockerfile: "docker",
  ps1: "powershell",
  ps: "powershell",
}

const packageManagerCommands: Record<string, PackageManager> = {
  npx: "npm",
  npm: "npm",
  yarn: "yarn",
  pnpm: "pnpm",
  bunx: "bun",
  bun: "bun",
}

function resolveLanguage(lang: string): string {
  return languageAliases[lang.toLowerCase()] || lang.toLowerCase()
}

function detectPackageManagerFromMarkdown(input: string): {
  isPackageManager: boolean
  command: string
  manager: PackageManager
} | null {
  const parsed = parseMarkdownCodeBlock(input)
  if (!parsed.language) return null

  const lang = parsed.language.toLowerCase()
  const manager = packageManagerCommands[lang]

  if (manager) {
    return {
      isPackageManager: true,
      command: parsed.code,
      manager,
    }
  }

  return null
}

function convertNpxToPackageManagers(npxCommand: string): {
  npm: string
  yarn: string
  pnpm: string
  bun: string
} {
  // Handle patterns like "npx shadcn@latest add button" or just "shadcn@latest add button"
  let command = npxCommand.trim()

  // Remove leading "npx " if present
  if (command.startsWith("npx ")) {
    command = command.slice(4)
  }

  return {
    npm: `npx ${command}`,
    yarn: `yarn dlx ${command}`,
    pnpm: `pnpm dlx ${command}`,
    bun: `bunx ${command}`,
  }
}


/* ------------------------------------------------------------------
 * Icons
 * ------------------------------------------------------------------
 * Inlined rather than imported from lucide-react. Three glyphs are not worth
 * an install-time dependency on a consumer's project: the registry item lists
 * what `shadcn add` installs, and every package there is one the reader did
 * not choose. The paths are lucide's own (ISC), drawn at its 24px grid so they
 * sit correctly beside icons a consumer does bring from it.
 */

interface IconProps {
  className?: string
  style?: React.CSSProperties
}

function iconProps({ className, style }: IconProps) {
  return {
    className,
    style,
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 2,
    strokeLinecap: "round" as const,
    strokeLinejoin: "round" as const,
    "aria-hidden": true,
  }
}

function CopyIcon(props: IconProps) {
  return (
    <svg {...iconProps(props)}>
      <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
      <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
    </svg>
  )
}

function CheckIcon(props: IconProps) {
  return (
    <svg {...iconProps(props)}>
      <path d="M20 6 9 17l-5-5" />
    </svg>
  )
}

function TerminalIcon(props: IconProps) {
  return (
    <svg {...iconProps(props)}>
      <polyline points="4 17 10 11 4 5" />
      <line x1="12" x2="20" y1="19" y2="19" />
    </svg>
  )
}

/** The tone every piece of chrome sitting ON the code surface shares — the
 *  copy glyph, the filename and its file badge, the package-manager tabs — so
 *  they read as one set rather than several separately-tuned greys.
 *
 *  Opacity on the INHERITED colour, never a colour of its own. 60% of a
 *  surface's own foreground IS the muted token: measured against both
 *  palettes it lands within 1/255 of `--muted-foreground` over `--card`, in
 *  light and in dark alike. This used to be two branches saying the same
 *  thing twice — the app's muted token when the block sat on the page's card,
 *  an opacity when it painted from its own palette — and only the opacity
 *  form follows the surface underneath it. Riding the inherited colour means
 *  a palette brought from elsewhere, AND a consumer recolouring the header
 *  with a className, both carry the chrome with them rather than stranding it
 *  on the page's grey.
 *
 *  Hover is the only thing that brightens it — deliberately not touch. A phone
 *  never hovers, so pinning it to full strength there (which is what this used
 *  to do) left the copy glyph permanently darker than the filename it shares a
 *  row with.
 *
 *  Both are complete literal class strings. Tailwind generates utilities by
 *  scanning source text, so a class assembled as `${modifier}:opacity-100`
 *  never reaches the stylesheet — a hover written that way here once did
 *  nothing at all. Keep them literal. */
const CHROME_REST = "opacity-60"
const CHROME_HOVER = "group-hover:opacity-100"

/** Copy ⇄ check swap: both glyphs stay mounted, stacked, and cross-morph —
 *  the leaver shrinks to a blurred point while the arriver grows out of one
 *  (same choreography as MP3Player's play/pause). The leaver takes the exit
 *  tier so the arriver is what the eye lands on. Spans, not the svgs, carry
 *  the transition: Button already owns `[&_svg]:transition` for its press dip. */
const ICON_SWAP =
  "absolute inset-0 flex items-center justify-center opacity-0 scale-25 blur-[4px] transition-[opacity,scale,filter] duration-(--motion-moderate-exit) ease-spring data-[shown=true]:opacity-100 data-[shown=true]:scale-100 data-[shown=true]:blur-none data-[shown=true]:duration-(--motion-moderate)"

/** Copy-to-clipboard control for a code surface.
 *
 *  Transparent at rest so it doesn't sit on the code as a visible chip; the
 *  ground arrives on hover, mixed against the block's own `--code-fg`/
 *  `--code-bg` (globals.css §8) rather than an app token, because a block
 *  carries its own light/dark palette independent of the page. Both vars are
 *  set once on the block's outer wrapper and inherited here, so this needs no
 *  colour prop of its own.
 *
 *  A floating button (no filename bar to sit in) also gets that ground at
 *  rest on a touch device — there's no hover to reveal it otherwise, so
 *  without a resting ground the glyph lands on top of whatever the first
 *  line happens to be. The two in-header buttons already sit on a bar and
 *  stay transparent until touched or hovered. */
function CopyButton({
  value,
  className,
  floating = false,
}: {
  value: string
  className?: string
  /** Floats over the code with no header/figcaption underneath it. */
  floating?: boolean
}) {
  const [hasCopied, setHasCopied] = useState(false)

  useEffect(() => {
    if (!hasCopied) return
    const timer = setTimeout(() => setHasCopied(false), 2000)
    return () => clearTimeout(timer)
  }, [hasCopied])

  return (
    <Button
      data-slot="copy-button"
      type="button"
      variant="ghost"
      size="icon-compact"
      aria-label={hasCopied ? "Copied" : "Copy code"}
      className={cn(
        "group size-7 border-none shadow-none transition-colors",
        "bg-transparent hover:bg-[var(--copy-button-hover-bg)] active:bg-[var(--copy-button-hover-bg)]",
        // Opt out of the ghost variant's own muted/foreground pair. The glyph
        // takes its colour from the bar it sits in, so recolouring the header
        // carries it too — tailwind-merge drops the variant's classes for
        // these, and the hover half has to be named or it snaps back on hover.
        "text-inherit hover:text-inherit",
        // No hover on a phone, so only the floating button — the one with no
        // ground under it already — gets a resting chip there.
        floating && "[@media(hover:none)]:bg-[var(--copy-button-rest-bg)]",
        className
      )}
      style={
        // Only the floating button, which has no header to take a colour
        // from. In a header bar it inherits — which is what lets a caller
        // recolour the bar and carry this glyph along with it.
        floating ? ({ color: "var(--code-fg)" } as React.CSSProperties) : undefined
      }
      onClick={async () => {
        // Only claim success when the copy actually landed.
        if (await copyToClipboard(value)) setHasCopied(true)
      }}
    >
      <span
        className={cn(
          "relative block size-3.5 transition-opacity",
          CHROME_REST,
          CHROME_HOVER,
          hasCopied && "opacity-100"
        )}
      >
        <span data-shown={!hasCopied} className={ICON_SWAP}>
          <CopyIcon className="size-3.5" />
        </span>
        <span data-shown={hasCopied} className={ICON_SWAP}>
          <CheckIcon className="size-3.5" />
        </span>
      </span>
    </Button>
  )
}

/** The highlighted source itself, with optional gutter line numbers. Shared by
 *  the expandable and plain wrappers so the two can't drift. */
function HighlightedCode({
  code,
  language,
  theme,
  showLineNumbers,
}: {
  code: string
  language: string
  theme: PrismTheme
  showLineNumbers: boolean
}) {
  const lineNumberColor = theme.plain?.color
    ? `color-mix(in srgb, ${theme.plain.color} 35%, transparent)`
    : "rgba(128, 128, 128, 0.35)"

  return (
    <Highlight theme={theme} code={code.trim()} language={language}>
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <pre
          data-slot="code-pre"
          className={cn(
            className,
            // w-max so the block grows to the longest line, min-w-full so it
            // still fills the scroller when every line is short. Together they
            // give the rows below a width to stretch to.
            "relative w-max min-w-full py-3.5 outline-none",
            // Unitless leading rather than `leading-6`: it lands on the same
            // 24px at the default 14px, but a `text-*` override on the root
            // now scales the rows with the type instead of leaving them
            // pinned to a height the larger text overflows.
            "leading-[1.7143] font-normal"
          )}
          style={
            {
              ...style,
              fontFamily: monoFontFamily,
              backgroundColor: "transparent",
              MozOsxFontSmoothing: "grayscale",
              "--line-number-color": lineNumberColor,
            } as React.CSSProperties
          }
        >
          {tokens.map((line, i) => (
            <div
              key={i}
              {...getLineProps({ line })}
              // A sticky child can only stay put inside its own containing
              // block. Left to size on its content, a short line's row ended
              // before the scroll did and its number slid away with it; at the
              // full block width every gutter survives the whole scroll.
              className="relative flex min-h-[1.7143em] w-max min-w-full"
            >
              {showLineNumbers && (
                // Sticky so the gutter survives a horizontal scroll; it paints
                // the block background so code scrolls *under* it, not through.
                <span
                  data-slot="code-gutter"
                  className="sticky left-0 z-10 w-16 flex-shrink-0 pr-6 pl-6 text-right font-medium tabular-nums select-none"
                  style={{
                    fontFamily: monoFontFamily,
                    color: lineNumberColor,
                    backgroundColor: "var(--code-bg)",
                  }}
                >
                  {i + 1}
                </span>
              )}
              <span
                className={cn(
                  "flex-1 pr-6 whitespace-pre",
                  !showLineNumbers && "pl-6"
                )}
              >
                {line.map((token, key) => (
                  <span key={key} {...getTokenProps({ token })} />
                ))}
              </span>
            </div>
          ))}
        </pre>
      )}
    </Highlight>
  )
}

const COLLAPSED_ROW_CAP = 40

export function Code({
  npm,
  yarn,
  pnpm,
  bun,
  defaultPackageManager = "npm",
  code,
  css,
  language = "typescript",
  defaultLanguage,
  filename,
  showLineNumbers = true,
  theme,
  adaptiveTheme,
  useThemeBackground,
  className,
  scrollbar = true,
  expandable = false,
  defaultExpanded = false,
  collapsedHeight = "12rem",
  expandLabel = "Expand",
  style,
  ...props
}: CodeProps) {
  const [packageManager, setPackageManager] = React.useState<PackageManager>(
    defaultPackageManager
  )
  const [isExpanded, setIsExpanded] = useState(defaultExpanded)

  // A caller-supplied palette brings its own background; the built-in pair is
  // designed to sit flush with --card, so only that one defaults to the token.
  const paintFromTheme = useThemeBackground ?? Boolean(theme || adaptiveTheme)

  const pageSurfaceVars = { color: "var(--foreground)", backgroundColor: "var(--card)" }

  // `adaptiveTheme` is the one case that still varies by resolved theme, but
  // the choice is made by the `.dark` class (see `wrapperVars` below), not by
  // reading which theme is on screen — so `selectedTheme` itself never
  // branches on light vs. dark.
  //
  // Without the theme's ground, its plain colour can't be trusted against the
  // page surface either — a dark palette's near-white body text would land on
  // a light card. Tokens keep their colours; unstyled text falls back.
  const selectedTheme =
    theme && !paintFromTheme
      ? { ...theme, plain: { ...theme.plain, ...pageSurfaceVars } }
      : theme || builtinTheme

  // The palette's foreground/background, set once on the outer wrapper so the
  // copy button's resting/hover ground (globals.css §8) can mix against a
  // block's real surface instead of `transparent`. `adaptiveTheme` sets both
  // halves of every pair here too, and `.code-adaptive-theme` (also in §8)
  // is what lets `.dark` pick between them without JS.
  const wrapperVars: Record<string, string> = adaptiveTheme
    ? {
        "--code-fg-l": adaptiveTheme.light.plain?.color ?? "",
        "--code-fg-d": adaptiveTheme.dark.plain?.color ?? "",
        "--code-bg-l": adaptiveTheme.light.plain?.backgroundColor ?? "",
        "--code-bg-d": adaptiveTheme.dark.plain?.backgroundColor ?? "",
        ...Object.fromEntries(
          tokenGroups.flatMap(({ key, types }) => [
            [`--code-token-${key}-l`, resolveGroupColor(adaptiveTheme.light, types)],
            [`--code-token-${key}-d`, resolveGroupColor(adaptiveTheme.dark, types)],
          ])
        ),
      }
    : {
        "--code-fg": theme?.plain?.color ?? "var(--foreground)",
        "--code-bg": theme?.plain?.backgroundColor ?? "var(--card)",
      }

  // The code surface paints straight from `--code-bg`, so turning the theme
  // ground off has to move the variable itself — the header alone would leave
  // the two halves of the block on different surfaces. Inline vars outrank the
  // `.code-adaptive-theme` class rules, so this covers the adaptive pair too.
  if (!paintFromTheme) {
    wrapperVars["--code-fg"] = pageSurfaceVars.color
    wrapperVars["--code-bg"] = pageSurfaceVars.backgroundColor
  }

  const packageManagerFromMarkdown = code
    ? detectPackageManagerFromMarkdown(code)
    : null

  const parsedMarkdown = code ? parseMarkdownCodeBlock(code) : null
  const actualCode = css || (parsedMarkdown?.code ?? code)
  const actualLanguage = css
    ? "css"
    : parsedMarkdown?.language
      ? resolveLanguage(parsedMarkdown.language)
      : defaultLanguage || language

  // Two ways to get a package-manager block: an explicit npm/yarn/pnpm/bun
  // prop set, or a ```npx fenced command we expand into all four. They render
  // the same tab strip, so they resolve to one shape here rather than forking
  // the JSX.
  const explicit: Partial<Record<PackageManager, string>> = {
    npm,
    yarn,
    pnpm,
    bun,
  }
  const hasExplicit = Object.values(explicit).some(Boolean)
  const pmCommands = packageManagerFromMarkdown
    ? convertNpxToPackageManagers(packageManagerFromMarkdown.command)
    : hasExplicit
      ? explicit
      : null

  if (pmCommands) {
    const keys = (["npm", "yarn", "pnpm", "bun"] as PackageManager[]).filter(
      (key) => pmCommands[key]
    )
    const active = pmCommands[packageManager] ? packageManager : keys[0]
    const surfaceStyle = paintFromTheme
      ? {
          backgroundColor: selectedTheme.plain?.backgroundColor,
          color: selectedTheme.plain?.color,
        }
      : undefined

    return (
      <div
        data-slot="code"
        className={cn(
          "bg-card text-card-foreground border-border/60 overflow-hidden rounded-[var(--radius-bg,var(--radius,0.5rem))] border [background-clip:padding-box]",
          CODE_TEXT_SIZE,
          adaptiveTheme && "code-adaptive-theme",
          className
        )}
        style={{ ...(wrapperVars as React.CSSProperties), ...style }}
        {...props}
      >
        <div
          data-slot="code-header"
          className={cn(
            "border-border/60 flex items-center justify-between border-b px-3 py-2",
            !paintFromTheme && "bg-background"
          )}
          style={surfaceStyle}
        >
          <div className="flex items-center gap-2.5">
            <TerminalIcon className={cn("size-4", CHROME_REST)} />
            <div
              role="tablist"
              aria-label="Package manager"
              data-slot="code-tablist"
              className="flex items-center"
            >
              {keys.map((key) => (
                <button
                  key={key}
                  type="button"
                  role="tab"
                  aria-selected={active === key}
                  data-state={active === key ? "active" : "inactive"}
                  data-slot="code-tab"
                  onClick={() => setPackageManager(key)}
                  className={cn(
                    "h-7 rounded-[var(--radius-item,var(--radius,0.5rem))] px-2.5 font-medium transition-opacity",
                    CHROME_REST,
                    "data-[state=active]:opacity-100",
                    "outline-none focus-visible:ring-1 focus-visible:ring-[color:var(--focus-ring,#6B97FF)]"
                  )}
                  style={{ fontFamily: monoFontFamily }}
                >
                  {key}
                </button>
              ))}
            </div>
          </div>
          <CopyButton value={pmCommands[active] || ""} className="size-7" />
        </div>
        <div
          data-slot="code-body"
          className={cn("overflow-x-auto", !scrollbar && "scrollbar-hide")}
        >
          <div
            className={cn("relative w-max min-w-full", !paintFromTheme && "bg-card")}
            style={
              paintFromTheme
                ? { backgroundColor: selectedTheme.plain?.backgroundColor }
                : undefined
            }
          >
            <Highlight
              theme={selectedTheme}
              code={pmCommands[active] || ""}
              language="bash"
            >
              {({
                className: highlightClassName,
                style,
                tokens,
                getLineProps,
                getTokenProps,
              }) => (
                <pre
                  data-slot="code-pre"
                  className={cn(
                    highlightClassName,
                    "w-max min-w-full px-4 py-4 leading-relaxed font-normal"
                  )}
                  style={{
                    ...style,
                    fontFamily: monoFontFamily,
                    backgroundColor: paintFromTheme
                      ? selectedTheme.plain?.backgroundColor
                      : "transparent",
                    WebkitFontSmoothing: "antialiased",
                    MozOsxFontSmoothing: "grayscale",
                  }}
                >
                  {tokens.map((line, i) => (
                    <div key={i} {...getLineProps({ line })}>
                      {line.map((token, key) => (
                        <span key={key} {...getTokenProps({ token })} />
                      ))}
                    </div>
                  ))}
                </pre>
              )}
            </Highlight>
          </div>
        </div>
      </div>
    )
  }

  if (!actualCode) return null

  // A collapsed block shows a handful of rows under `collapsedHeight`, and
  // tokenising and mounting the other hundreds — several hundred milliseconds
  // for a long source file — is what froze a tab switch that revealed one.
  // Until it is expanded only the rows that can be seen are rendered; the
  // copy button still carries the whole thing.
  // ponytail: 40 rows covers a collapsedHeight of ~4× the default; derive
  // the count from the height if a caller ever collapses taller than that.
  const collapsed = expandable && !isExpanded
  const visibleCode = collapsed
    ? actualCode.split("\n").slice(0, COLLAPSED_ROW_CAP).join("\n")
    : actualCode

  const highlighted = (
    <HighlightedCode
      code={visibleCode}
      language={actualLanguage}
      theme={selectedTheme}
      showLineNumbers={showLineNumbers}
    />
  )

  return (
    <div
      data-slot="code"
      className={cn(
        "pointer-events-auto w-full max-w-full overflow-hidden",
        "border-border/60 border [background-clip:padding-box]",
        "rounded-[var(--radius-bg,var(--radius,0.5rem))]",
        CODE_TEXT_SIZE,
        adaptiveTheme && "code-adaptive-theme",
        className
      )}
      style={{ ...(wrapperVars as React.CSSProperties), ...style }}
      {...props}
    >
      {filename && (
        <figcaption
          data-slot="code-header"
          className={cn(
            "border-border/60 flex items-center justify-between border-b px-4 py-2.5",
            !paintFromTheme && "bg-background"
          )}
          style={{
            fontFamily: monoFontFamily,
            ...(paintFromTheme
              ? {
                  backgroundColor: selectedTheme.plain?.backgroundColor,
                  color: selectedTheme.plain?.color,
                }
              : {}),
          }}
        >
          <div
            data-slot="code-filename"
            className={cn("flex items-center gap-2", CHROME_REST)}
          >
            {/* The file badge is a solid shape where the name beside it is
                13px text, so at one shared colour it out-weighs the name it
                belongs to and pulls the eye off both the label and the copy
                button. The token stays the same; a notch of transparency is
                what evens the three optically. */}
            <span className="flex items-center opacity-70 [&_svg]:size-3.5">
              {getIconForFile(filename)}
            </span>
            <span className="font-medium tracking-tight">{filename}</span>
          </div>
          <div className="flex items-center gap-2">
            <CopyButton value={actualCode} />
          </div>
        </figcaption>
      )}

      <div className="relative">
        {/* With no filename bar to live in, the controls float over the code.
            The row is pointer-transparent so it doesn't eat text selection;
            only the controls themselves take pointer events back. */}
        {!filename && (
          <div className="pointer-events-none absolute inset-x-0 top-0 z-10 flex justify-end">
            <div className="pointer-events-auto flex items-center gap-2 p-3">
              <CopyButton value={actualCode} floating />
            </div>
          </div>
        )}

        {expandable ? (
          <div
            data-slot="code-body"
            className={cn(
              "relative w-full transition-all",
              isExpanded ? "overflow-auto" : "overflow-hidden",
              !scrollbar && "scrollbar-hide"
            )}
            style={{
              backgroundColor: "var(--code-bg)",
              maxHeight: isExpanded ? "none" : collapsedHeight,
            }}
          >
            {highlighted}
          </div>
        ) : (
          <div
            data-slot="code-body"
            className={cn(
              "relative max-h-[450px] w-full overflow-auto",
              !scrollbar && "scrollbar-hide"
            )}
            style={{ backgroundColor: "var(--code-bg)" }}
          >
            {highlighted}
          </div>
        )}

        {expandable && !isExpanded && (
          <div
            data-slot="code-expand-overlay"
            className="absolute inset-x-0 bottom-0 z-20 flex h-24 items-end justify-center rounded-b-[var(--radius-bg,var(--radius,0.5rem))] pb-4"
            style={{
              background: `linear-gradient(to top, ${
                paintFromTheme && selectedTheme.plain?.backgroundColor
                  ? selectedTheme.plain.backgroundColor
                  : "var(--card)"
              } 40%, color-mix(in oklab, ${
                paintFromTheme && selectedTheme.plain?.backgroundColor
                  ? selectedTheme.plain.backgroundColor
                  : "var(--card)"
              } 60%, transparent) 75%, transparent 100%)`,
            }}
          >
            <Button
              type="button"
              data-slot="code-expand"
              variant="tertiary"
              size="compact"
              onClick={() => setIsExpanded(true)}
              className="relative z-10 shadow-none"
            >
              {expandLabel}
            </Button>
          </div>
        )}
      </div>
    </div>
  )
}
registry/ui/code-icons.tsx
import type React from "react"

type IconProps = React.HTMLAttributes<SVGElement>

export const Icons = {
  ts: (props: IconProps) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
      <path
        fill="currentColor"
        d="M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z"
      />
    </svg>
  ),
  css: (props: IconProps) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
      <path
        fill="currentColor"
        d="M1.5 0h21l-1.91 21.563L11.977 24l-8.565-2.438L1.5 0zm17.09 4.413L5.41 4.41l.213 2.622 10.125.002-.255 2.716h-6.64l.24 2.573h6.182l-.366 3.523-2.91.804-2.956-.81-.188-2.11h-2.61l.29 3.855L12 19.288l5.373-1.53L18.59 4.414z"
      />
    </svg>
  ),
  json: (props: IconProps) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
      <path
        fill="currentColor"
        d="M5.759 3.975h1.783V5.76H5.759v4.458A1.783 1.783 0 0 1 3.975 12a1.783 1.783 0 0 1 1.784 1.783v4.459h1.783v1.783H5.759c-.954-.24-1.784-.803-1.784-1.783v-3.567a1.783 1.783 0 0 0-1.783-1.783H.975v-1.783h1.217a1.783 1.783 0 0 0 1.783-1.784V5.76c0-.98.83-1.543 1.784-1.784m12.482 0c.954.24 1.784.803 1.784 1.783v3.567a1.783 1.783 0 0 0 1.783 1.783h1.217v1.784h-1.217a1.783 1.783 0 0 0-1.783 1.783v3.567c0 .98-.83 1.543-1.784 1.783h-1.783v-1.783h1.783v-4.459A1.783 1.783 0 0 1 20.025 12a1.783 1.783 0 0 1-1.784-1.783V5.759h-1.783V3.975z"
      />
    </svg>
  ),
  python: (props: IconProps) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
      <path
        fill="currentColor"
        d="M11.914 0C5.82 0 6.2 2.656 6.2 2.656l.007 2.752h5.814v.826H3.9S0 5.789 0 11.969c0 6.18 3.403 5.96 3.403 5.96h2.03v-2.867s-.109-3.42 3.35-3.42h5.766s3.24.052 3.24-3.148V3.202S18.28 0 11.914 0M8.708 1.85c.578 0 1.046.47 1.046 1.052 0 .581-.468 1.051-1.046 1.051-.579 0-1.046-.47-1.046-1.051 0-.582.467-1.052 1.046-1.052"
      />
      <path
        fill="currentColor"
        d="M12.086 24c6.094 0 5.714-2.656 5.714-2.656l-.007-2.752h-5.814v-.826h8.121s3.9.445 3.9-5.735c0-6.18-3.403-5.96-3.403-5.96h-2.03v2.867s.109 3.42-3.35 3.42H9.451s-3.24-.052-3.24 3.148v5.292S5.72 24 12.086 24m3.206-1.85c-.578 0-1.046-.47-1.046-1.052 0-.581.468-1.051 1.046-1.051.579 0 1.046.47 1.046 1.051 0 .582-.467 1.052-1.046 1.052"
      />
    </svg>
  ),
  rust: (props: IconProps) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="22 22 100 100" {...props}>
      <path
        fill="currentColor"
        d="m71.05 23.68c-26.06 0-47.27 21.22-47.27 47.27s21.22 47.27 47.27 47.27 47.27-21.22 47.27-47.27-21.22-47.27-47.27-47.27zm-.07 4.2a3.1 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm7.12 5.12a38.27 38.27 0 0 1 26.2 18.66l-3.67 8.28c-.63 1.43.02 3.11 1.44 3.75l7.06 3.13a38.27 38.27 0 0 1 .08 6.64h-3.93c-.39 0-.55.26-.55.64v1.8c0 4.24-2.39 5.17-4.49 5.4-2 .23-4.21-.84-4.49-2.06-1.18-6.63-3.14-8.04-6.24-10.49 3.85-2.44 7.85-6.05 7.85-10.87 0-5.21-3.57-8.49-6-10.1-3.42-2.25-7.2-2.7-8.22-2.7h-40.6a38.27 38.27 0 0 1 21.41-12.08l4.79 5.02c1.08 1.13 2.87 1.18 4 .09zm-44.2 23.02a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm74.15.14a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm-68.29.5h5.42v24.44h-10.94a38.27 38.27 0 0 1 -1.24-14.61l6.7-2.98c1.43-.64 2.08-2.31 1.44-3.74zm22.62.26h12.91c.67 0 4.71.77 4.71 3.8 0 2.51-3.1 3.41-5.65 3.41h-11.98zm0 17.56h9.89c.9 0 4.83.26 6.08 5.28.39 1.54 1.26 6.56 1.85 8.17.59 1.8 2.98 5.4 5.53 5.4h16.14a38.27 38.27 0 0 1 -3.54 4.1l-6.57-1.41c-1.53-.33-3.04.65-3.37 2.18l-1.56 7.28a38.27 38.27 0 0 1 -31.91-.15l-1.56-7.28c-.33-1.53-1.83-2.51-3.36-2.18l-6.43 1.38a38.27 38.27 0 0 1 -3.32-3.92h31.27c.35 0 .59-.06.59-.39v-11.06c0-.32-.24-.39-.59-.39h-9.15zm-14.43 25.33a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm46.05.14a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11z"
      />
      <path
        fill="currentColor"
        fillRule="evenodd"
        stroke="currentColor"
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth="3"
        d="m115.68 70.95a44.63 44.63 0 0 1 -44.63 44.63 44.63 44.63 0 0 1 -44.63-44.63 44.63 44.63 0 0 1 44.63-44.63 44.63 44.63 0 0 1 44.63 44.63zm-.84-4.31 6.96 4.31-6.96 4.31 5.98 5.59-7.66 2.87 4.78 6.65-8.09 1.32 3.4 7.46-8.19-.29 1.88 7.98-7.98-1.88.29 8.19-7.46-3.4-1.32 8.09-6.65-4.78-2.87 7.66-5.59-5.98-4.31 6.96-4.31-6.96-5.59 5.98-2.87-7.66-6.65 4.78-1.32-8.09-7.46 3.4.29-8.19-7.98 1.88 1.88-7.98-8.19.29 3.4-7.46-8.09-1.32 4.78-6.65-7.66-2.87 5.98-5.59-6.96-4.31 6.96-4.31-5.98-5.59 7.66-2.87-4.78-6.65 8.09-1.32-3.4-7.46 8.19.29-1.88-7.98 7.98 1.88-.29-8.19 7.46 3.4 1.32-8.09 6.65 4.78 2.87-7.66 5.59 5.98 4.31-6.96 4.31 6.96 5.59-5.98 2.87 7.66 6.65-4.78 1.32 8.09 7.46-3.4-.29 8.19 7.98-1.88-1.88 7.98 8.19-.29-3.4 7.46 8.09 1.32-4.78 6.65 7.66 2.87z"
      />
    </svg>
  ),
  bash: (props: IconProps) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
      <path
        fill="currentColor"
        d="M21.038 4.9 13.461.402a2.86 2.86 0 0 0-2.923.001L2.961 4.9A3.023 3.023 0 0 0 1.5 7.503v8.995c0 1.073.557 2.066 1.462 2.603l7.577 4.497a2.86 2.86 0 0 0 2.922 0l7.577-4.497a3.023 3.023 0 0 0 1.462-2.603V7.503A3.021 3.021 0 0 0 21.038 4.9z"
      />
    </svg>
  ),
  html: (props: IconProps) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
      <path
        fill="currentColor"
        d="M1.5 0h21l-1.91 21.563L11.977 24l-8.564-2.438L1.5 0Zm7.031 9.75-.232-2.718 10.059.003.23-2.622L5.412 4.41l.698 8.01h9.126l-.326 3.426-2.91.804-2.955-.81-.188-2.11H6.248l.33 4.171L12 19.351l5.379-1.443.744-8.157H8.531Z"
      />
    </svg>
  ),
  react: (props: IconProps) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
      <path
        fill="currentColor"
        d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38a2.167 2.167 0 0 0-1.092-.278z"
      />
    </svg>
  ),
  go: (props: IconProps) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
      <path
        fill="currentColor"
        d="M1.811 10.715c-.047 0-.058-.024-.035-.059l.246-.316c.024-.035.078-.059.129-.059h4.17c.047 0 .058.035.035.07l-.199.305c-.02.035-.082.07-.117.07zm-1.35 1.073c-.047 0-.058-.023-.035-.058l.246-.316c.023-.035.082-.059.129-.059h5.328c.047 0 .07.035.059.07l-.094.28c-.012.046-.059.07-.106.07zm2.156 1.074c-.047 0-.058-.035-.035-.07l.164-.293c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.281c0 .047-.047.082-.082.082zM15.004 10.505c-.735.188-1.239.328-1.963.516-.176.047-.188.059-.34-.117-.176-.199-.305-.328-.551-.445-.738-.363-1.452-.258-2.115.176-.797.516-1.207 1.276-1.195 2.22.012.938.657 1.708 1.582 1.836.797.106 1.464-.164 1.992-.773.106-.129.2-.27.317-.434h-2.258c-.246 0-.305-.152-.223-.352.152-.363.434-.973.598-1.276.047-.082.14-.188.293-.188h4.253c-.024.317-.024.633-.07.95-.118.809-.4 1.558-.856 2.226-.703 1.027-1.64 1.734-2.844 2.033-1.002.246-1.968.176-2.868-.293-.856-.446-1.441-1.123-1.758-2.015-.363-1.027-.317-2.033.129-3.001.504-1.095 1.301-1.898 2.396-2.391 1.002-.445 2.045-.527 3.118-.258.797.199 1.476.61 2.01 1.229.164.199.305.41.48.656h.012zM18.989 16.847c-.925-.012-1.78-.27-2.52-.833-.633-.48-1.038-1.1-1.16-1.874-.176-1.12.152-2.092.867-2.938.668-.797 1.523-1.289 2.555-1.464 1.003-.164 1.945-.023 2.808.539.75.492 1.254 1.158 1.43 2.057.222 1.27-.118 2.32-.949 3.236-.609.68-1.37 1.135-2.25 1.265-.422.07-.845.07-1.278.082z"
      />
    </svg>
  ),
  markdown: (props: IconProps) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" {...props}>
      <path
        fill="currentColor"
        d="M102 366C88 366 76 354 76 340V170c0-14 12-26 26-26h307c14 0 26 12 26 26v170c0 14-11 26-25 26zM102 162c-4 0-9 4-9 9v170c0 5 4 9 9 9h307c5 0 9-4 9-9V170c0-5-4-9-9-9H102zm26 153V196h34l34 43 34-43h35v119h-34v-68l-34 43-34-43v68zm216 0-52-57h34v-61h34v60h34z"
      />
    </svg>
  ),
  file: (props: IconProps) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
      <path
        fill="none"
        stroke="currentColor"
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth="2"
        d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
      />
      <polyline
        fill="none"
        stroke="currentColor"
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth="2"
        points="14 2 14 8 20 8"
      />
    </svg>
  ),
}

export function getIconForFile(filename: string) {
  const extension = filename.split(".").pop()?.toLowerCase()

  switch (extension) {
    case "json":
      return <Icons.json className="size-4 fill-current" />
    case "css":
      return <Icons.css className="size-4 fill-current" />
    case "ts":
    case "tsx":
    case "typescript":
      return <Icons.ts className="size-4 fill-current" />
    case "js":
    case "jsx":
      return <Icons.react className="size-4 fill-current" />
    case "py":
    case "python":
      return <Icons.python className="size-4 fill-current" />
    case "rs":
    case "rust":
      return <Icons.rust className="size-4 fill-current" />
    case "sh":
    case "bash":
    case "zsh":
      return <Icons.bash className="size-4 fill-current" />
    case "html":
    case "htm":
      return <Icons.html className="size-4 fill-current" />
    case "go":
      return <Icons.go className="size-4 fill-current" />
    case "md":
    case "mdx":
      return <Icons.markdown className="size-4 fill-current" />
    default:
      return <Icons.file className="size-4" />
  }
}
registry/lib/prism-languages.ts
/**
 * Register additional Prism grammars on the shared instance used by
 * ``prism-react-renderer``.
 *
 * ``prism-react-renderer`` ships with a small built-in set (JS/TS,
 * Python, Go, Rust, JSON, YAML, CSS/HTML, SQL, Swift, Kotlin, etc.)
 * and silently falls back to a plain-text render for anything not on
 * that list. Each language-component file from ``prismjs/components``
 * self-registers on ``globalThis.Prism`` as a side effect of being
 * imported — so we expose the renderer's Prism instance there once,
 * then ``require`` each grammar we want to support.
 *
 * Importing this module anywhere in the app is enough; the
 * registrations are global and idempotent. Keep this file
 * side-effect-only: no exports, no React, no SSR-unsafe globals.
 */

import { Prism } from "prism-react-renderer";

// ``prismjs`` grammars read from ``global.Prism`` (Node) or
// ``window.Prism`` (browser). ``globalThis`` resolves to whichever is
// available, so this works on both the server and the client.
(globalThis as typeof globalThis & { Prism?: typeof Prism }).Prism = Prism;

// Each ``require`` mutates ``globalThis.Prism.languages`` and returns
// nothing useful — the side effect is the point. Using ``require``
// rather than ``import`` because the grammar files are CommonJS and
// expect a synchronous registration order.
/* eslint-disable @typescript-eslint/no-require-imports */
// ``prism-markup-templating`` defines ``Prism.languages.markup-templating``
// and helpers (``tokenizePlaceholders``) that PHP / Smarty / Twig /
// EJS / ERB grammars look up at registration time. It must load
// **before** any of those.
require("prismjs/components/prism-markup-templating");
require("prismjs/components/prism-bash");
require("prismjs/components/prism-diff");
require("prismjs/components/prism-docker");
require("prismjs/components/prism-ini");
require("prismjs/components/prism-java");
require("prismjs/components/prism-nginx");
require("prismjs/components/prism-php");
require("prismjs/components/prism-powershell");
require("prismjs/components/prism-ruby");
require("prismjs/components/prism-toml");
/* eslint-enable @typescript-eslint/no-require-imports */

More from Delta Components

Browse all components in this library →