{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"code-block","type":"registry:ui","title":"Code Block","description":"Code to read and copy, colored for its language, with a file name or tabs, line numbers and marked lines.","dependencies":[],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-code-tokens.json","https://dashral.com/r/copy-button.json"],"files":[{"path":"registry/dashral/code-block.tsx","type":"registry:file","target":"components/dashral/code-block.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\n\nimport { tokenizeLines, type CodeTokenKind } from \"./lib/code-tokens\"\nimport { cn } from \"./lib/utils\"\nimport { CopyButton } from \"./copy-button\"\n\ntype CodeTab = {\n  /** The tab's name: \"pnpm\", \"npm\", \"app.tsx\". */\n  label: string\n  code: string\n  language?: string\n}\n\ntype CodeBlockProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** The code, as plain text. */\n  code?: string\n  /**\n   * Shown at the end of the header (\"tsx\", \"bash\"), and what the code is colored as: ts, tsx, js, jsx, json, css,\n   * html and bash get colors; any other language shows plain.\n   */\n  language?: string\n  /** Colors the code by its language. On by default; false shows it plain. */\n  highlight?: boolean\n  /** A file name in the header: \"app/page.tsx\". */\n  filename?: string\n  /** Several snippets in one block, one at a time, e.g. a command for each package manager. In place of code. */\n  tabs?: CodeTab[]\n  /** The tab shown first (its label); the first by default. */\n  defaultTab?: string\n  /** Called with the tab's label when another is picked, e.g. to remember the reader's package manager. */\n  onTabChange?: (label: string) => void\n  /** Numbers each line. */\n  lineNumbers?: boolean\n  /** Line numbers (from 1) to mark, e.g. the ones a step changed. */\n  highlightLines?: number[]\n  /** Long lines wrap instead of scrolling sideways. */\n  wrap?: boolean\n  /** Scrolls past this height, in pixels or any CSS length. */\n  maxHeight?: number | string\n  /** The copy button in the corner. On by default. */\n  copyable?: boolean\n  /**\n   * Your own highlighted markup in place of the lines, e.g. from a highlighter run on the server, for a language\n   * CodeBlock doesn't color or for exact grammar. code is still what's copied. lineNumbers and highlightLines need the\n   * component's own lines, so they're ignored with it.\n   */\n  children?: React.ReactNode\n  classNames?: { header?: string; pre?: string; line?: string }\n}\n\n// The colors: only keywords, strings, tags, numbers and comments get one; names and punctuation stay in the text\n// color. Each reads a CSS variable, so a site can set its own (--code-keyword, --code-string, --code-tag,\n// --code-number, --code-comment); without them these are the defaults, for light and dark.\nconst tokenColors: Record<CodeTokenKind, string> = {\n  keyword: \"text-[color:var(--code-keyword,oklch(0.5_0.17_320))] dark:text-[color:var(--code-keyword,oklch(0.78_0.14_320))]\",\n  string: \"text-[color:var(--code-string,oklch(0.48_0.13_155))] dark:text-[color:var(--code-string,oklch(0.8_0.12_155))]\",\n  tag: \"text-[color:var(--code-tag,oklch(0.5_0.16_258))] dark:text-[color:var(--code-tag,oklch(0.78_0.12_255))]\",\n  number: \"text-[color:var(--code-number,oklch(0.55_0.15_55))] dark:text-[color:var(--code-number,oklch(0.82_0.12_70))]\",\n  comment: \"text-[color:var(--code-comment,var(--muted-foreground))] italic\",\n}\n\n/**\n * Code to read and copy, in one tag: `<CodeBlock filename=\"app/page.tsx\" language=\"tsx\" code={source} lineNumbers />`.\n * A header with the file name or tabs, a copy button, line numbers and marked lines, and the code colored by its\n * language (lib/code-tokens: TypeScript and JavaScript with JSX, JSON, CSS, HTML and shell). For another language,\n * pass your highlighter's output as children. Code always runs left to right, Arabic pages included.\n */\nfunction CodeBlock({\n  code: codeProp,\n  language: languageProp,\n  highlight = true,\n  filename,\n  tabs,\n  defaultTab,\n  onTabChange,\n  lineNumbers = false,\n  highlightLines,\n  wrap = false,\n  maxHeight,\n  copyable = true,\n  className,\n  classNames,\n  children,\n  ...props\n}: CodeBlockProps) {\n  const baseId = React.useId()\n  const [tab, setTab] = React.useState(defaultTab ?? tabs?.[0]?.label)\n  const activeTab = tabs?.find((t) => t.label === tab) ?? tabs?.[0]\n  const code = (activeTab?.code ?? codeProp ?? \"\").replace(/\\n$/, \"\")\n  const language = activeTab?.language ?? languageProp\n  const marked = new Set(highlightLines)\n  const lines = code.split(\"\\n\")\n  const colored = highlight && children === undefined ? tokenizeLines(code, language) : null\n  const hasHeader = Boolean(filename || tabs?.length || language)\n  const tabRefs = React.useRef<(HTMLButtonElement | null)[]>([])\n\n  function pick(label: string) {\n    setTab(label)\n    onTabChange?.(label)\n  }\n\n  // Tabs pattern: the arrows move between tabs (in reading order), Home and End to the ends.\n  function onTabKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, index: number) {\n    if (!tabs) return\n    const rtl = getComputedStyle(event.currentTarget).direction === \"rtl\"\n    const step = { ArrowRight: rtl ? -1 : 1, ArrowLeft: rtl ? 1 : -1 }[event.key]\n    const next = step !== undefined ? (index + step + tabs.length) % tabs.length : event.key === \"Home\" ? 0 : event.key === \"End\" ? tabs.length - 1 : -1\n    if (next < 0) return\n    event.preventDefault()\n    pick(tabs[next]!.label)\n    tabRefs.current[next]?.focus()\n  }\n\n  const copy = copyable && <CopyButton value={code} className=\"shrink-0\" />\n\n  return (\n    <div\n      data-slot=\"code-block\"\n      className={cn(\"group/code relative min-w-0 overflow-hidden rounded-lg border bg-muted/40 text-sm\", className)}\n      {...props}\n    >\n      {hasHeader && (\n        <div className={cn(\"flex h-10 items-center gap-2 border-b bg-muted/40 ps-3 pe-1.5\", classNames?.header)}>\n          {tabs?.length ? (\n            <div role=\"tablist\" className=\"-mb-px flex h-full min-w-0 items-stretch gap-3 overflow-x-auto\">\n              {tabs.map((t, i) => {\n                const selected = t.label === activeTab?.label\n                return (\n                  <button\n                    key={t.label}\n                    ref={(node) => {\n                      tabRefs.current[i] = node\n                    }}\n                    type=\"button\"\n                    role=\"tab\"\n                    id={`${baseId}-tab-${i}`}\n                    aria-selected={selected}\n                    aria-controls={`${baseId}-panel`}\n                    tabIndex={selected ? 0 : -1}\n                    onClick={() => pick(t.label)}\n                    onKeyDown={(event) => onTabKeyDown(event, i)}\n                    className={cn(\n                      \"shrink-0 border-b-2 px-0.5 font-mono text-xs outline-none transition-colors focus-visible:text-foreground focus-visible:underline\",\n                      selected ? \"border-foreground text-foreground\" : \"border-transparent text-muted-foreground hover:text-foreground\"\n                    )}\n                  >\n                    {t.label}\n                  </button>\n                )\n              })}\n            </div>\n          ) : (\n            filename && <span className=\"min-w-0 truncate font-mono text-xs text-muted-foreground\">{filename}</span>\n          )}\n          <span className=\"ms-auto flex shrink-0 items-center gap-1\">\n            {language && <span className=\"px-1.5 font-mono text-[11px] text-muted-foreground uppercase\">{language}</span>}\n            {copy}\n          </span>\n        </div>\n      )}\n      {!hasHeader && copy && (\n        // Without a header, the copy button floats in the corner, over the code.\n        <span className=\"absolute end-1.5 top-1.5 z-10 rounded-md bg-muted/80 backdrop-blur-sm\">{copy}</span>\n      )}\n      <pre\n        id={tabs?.length ? `${baseId}-panel` : undefined}\n        role={tabs?.length ? \"tabpanel\" : undefined}\n        aria-labelledby={tabs?.length ? `${baseId}-tab-${tabs.findIndex((t) => t.label === activeTab?.label)}` : undefined}\n        dir=\"ltr\"\n        tabIndex={0}\n        data-language={language}\n        style={{ maxHeight: typeof maxHeight === \"number\" ? `${maxHeight}px` : maxHeight }}\n        className={cn(\n          \"overflow-auto py-3 font-mono text-[13px] leading-6 outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset\",\n          wrap ? \"whitespace-pre-wrap break-words\" : \"whitespace-pre\",\n          children !== undefined && \"px-4\",\n          !hasHeader && copy && \"pe-12\",\n          classNames?.pre\n        )}\n      >\n        {children !== undefined ? (\n          children\n        ) : (\n          <code className={cn(\"grid\", wrap ? \"min-w-0\" : \"min-w-max\")}>\n            {lines.map((line, i) => (\n              <span\n                key={i}\n                data-highlighted={marked.has(i + 1) || undefined}\n                className={cn(\n                  \"flex px-4\",\n                  marked.has(i + 1) && \"border-s-2 border-primary bg-primary/8 ps-[14px]\",\n                  classNames?.line\n                )}\n              >\n                {lineNumbers && (\n                  <span\n                    aria-hidden\n                    style={{ width: `${Math.max(2, String(lines.length).length)}ch` }}\n                    className=\"me-4 inline-block shrink-0 text-end text-muted-foreground/70 select-none tabular-nums\"\n                  >\n                    {i + 1}\n                  </span>\n                )}\n                {/* An empty line still takes its height. */}\n                <span className=\"min-w-0 flex-1\">\n                  {colored?.[i]?.length\n                    ? colored[i]!.map((token, index) =>\n                        token.kind ? (\n                          <span key={index} className={tokenColors[token.kind]}>\n                            {token.text}\n                          </span>\n                        ) : (\n                          token.text\n                        )\n                      )\n                    : line || \" \"}\n                </span>\n              </span>\n            ))}\n          </code>\n        )}\n      </pre>\n    </div>\n  )\n}\n\nexport { CodeBlock, type CodeBlockProps, type CodeTab }\n"}]}