{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "snippet-tabs",
  "title": "Snippet Tabs",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Request examples in several languages with built-in highlighting, a remembered language choice shared across the page, line numbers, copy, and {{variables}} filled with real values.",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/copy-button.json",
    "https://ui.ballmac.com/r/highlight.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/snippet-tabs.tsx",
      "content": "// Ballmac UI: Snippet Tabs. https://ui.ballmac.com/components/snippet-tabs\n\"use client\"\n\nimport * as React from \"react\"\nimport { Tabs as TabsPrimitive } from \"radix-ui\"\n\nimport { CopyButton } from \"@/components/ballmac/copy-button\"\nimport { highlightLines, languageFromName, tokenClass, type HighlightLanguage } from \"@/lib/ballmac/highlight\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype Snippet = {\n  /** Tab text, such as \"cURL\" or \"Python\". Also picks the highlighter unless `language` is set. */\n  label: string\n  /** The code. `{{name}}` placeholders are replaced from the `variables` prop. */\n  code: string\n  /** Highlighter to use. Defaults to a guess from `label`. */\n  language?: HighlightLanguage\n}\n\nconst memory = new Map<string, string>()\nconst listeners = new Map<string, Set<() => void>>()\n\nfunction readStored(key: string): string | null {\n  if (memory.has(key)) return memory.get(key) ?? null\n  try {\n    return window.localStorage.getItem(key)\n  } catch {\n    return null\n  }\n}\n\nfunction writeStored(key: string, value: string) {\n  memory.set(key, value)\n  try {\n    window.localStorage.setItem(key, value)\n  } catch {\n    // Storage is blocked; the in-memory value still syncs this page.\n  }\n  listeners.get(key)?.forEach((notify) => notify())\n}\n\nfunction subscribeStored(key: string, notify: () => void) {\n  let set = listeners.get(key)\n  if (!set) listeners.set(key, (set = new Set()))\n  set.add(notify)\n  const onStorage = (event: StorageEvent) => {\n    if (event.key !== key) return\n    if (event.newValue === null) memory.delete(key)\n    else memory.set(key, event.newValue)\n    notify()\n  }\n  window.addEventListener(\"storage\", onStorage)\n  return () => {\n    set.delete(notify)\n    window.removeEventListener(\"storage\", onStorage)\n  }\n}\n\nfunction useStoredLabel(key: string | undefined) {\n  const subscribe = React.useCallback((notify: () => void) => (key ? subscribeStored(key, notify) : () => {}), [key])\n  return React.useSyncExternalStore(\n    subscribe,\n    () => (key ? readStored(key) : null),\n    // Server and hydration never see storage, so markup always matches.\n    () => null\n  )\n}\n\nfunction fill(code: string, variables?: Record<string, string>) {\n  if (!variables) return code\n  return code.replace(/\\{\\{\\s*([\\w.-]+)\\s*\\}\\}/g, (match, name: string) => variables[name] ?? match)\n}\n\ntype SnippetTabsProps = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"dir\" | \"title\"> & {\n  /** One tab per language. */\n  snippets: Snippet[]\n  /** Heading on the left of the tab bar, such as \"Create a customer\". */\n  title?: string\n  /** Label of the tab open at first. */\n  defaultValue?: string\n  /** Controlled open tab (a label). */\n  value?: string\n  /** Called with the label of the newly selected tab. */\n  onValueChange?: (label: string) => void\n  /** Remember the reader's language in localStorage under this key. Every instance sharing the key switches together. */\n  storageKey?: string\n  /** Values for `{{name}}` placeholders in the code, for example the viewer's API key. */\n  variables?: Record<string, string>\n  /** Show line numbers. */\n  lineNumbers?: boolean\n  /** Classes for the scrolling code area, for example a max height. */\n  bodyClassName?: string\n}\n\nfunction SnippetTabs({\n  snippets,\n  title,\n  defaultValue,\n  value: valueProp,\n  onValueChange,\n  storageKey,\n  variables,\n  lineNumbers = false,\n  bodyClassName,\n  className,\n  ...props\n}: SnippetTabsProps) {\n  const msg = useMessages()\n  const labels = snippets.map((s) => s.label)\n  const stored = useStoredLabel(storageKey)\n  const [internal, setInternal] = React.useState(defaultValue ?? labels[0] ?? \"\")\n  const preferred = stored && labels.includes(stored) ? stored : null\n  const value = valueProp ?? preferred ?? internal\n  const titleId = React.useId()\n\n  function select(next: string) {\n    setInternal(next)\n    if (storageKey) writeStored(storageKey, next)\n    onValueChange?.(next)\n  }\n\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"snippet-tabs\"\n      value={value}\n      onValueChange={select}\n      className={cn(\"w-full overflow-hidden rounded-xl border bg-card text-card-foreground\", className)}\n      {...props}\n    >\n      <div className=\"flex min-h-11 items-center gap-2 border-b bg-muted/40 pe-1.5 ps-1.5\">\n        {title && (\n          <span id={titleId} className=\"hidden shrink-0 ps-2 text-[13px] font-medium text-foreground sm:block\">\n            {title}\n          </span>\n        )}\n        <TabsPrimitive.List\n          aria-label={title ?? msg(\"snippet-tabs.language\", \"Language\")}\n          className=\"flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto py-1 [scrollbar-width:none] sm:justify-end [&::-webkit-scrollbar]:hidden\"\n        >\n          {snippets.map((s) => (\n            <TabsPrimitive.Trigger\n              key={s.label}\n              value={s.label}\n              className=\"relative h-7 shrink-0 rounded-md px-2.5 text-[13px] font-medium text-muted-foreground outline-none transition-colors duration-150 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-xs motion-reduce:transition-none\"\n            >\n              {s.label}\n            </TabsPrimitive.Trigger>\n          ))}\n        </TabsPrimitive.List>\n        <CopyButton\n          size=\"sm\"\n          ariaLabel=\"Copy snippet\"\n          getValue={() => fill(snippets.find((s) => s.label === value)?.code ?? \"\", variables)}\n        />\n      </div>\n      {snippets.map((s) => {\n        const text = fill(s.code, variables)\n        const lines = highlightLines(text, s.language ?? languageFromName(s.label))\n        return (\n          <TabsPrimitive.Content\n            key={s.label}\n            value={s.label}\n            tabIndex={-1}\n            className=\"outline-none data-[state=inactive]:hidden\"\n          >\n            <pre dir=\"ltr\"\n              role=\"region\"\n              aria-label={msg(\"snippet-tabs.code\", \"{label} code\", { label: s.label })}\n              tabIndex={0}\n              className={cn(\n                \"overflow-auto p-4 font-mono text-[13px] leading-6 text-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50\",\n                bodyClassName\n              )}\n            >\n              <code className=\"grid\">\n                {lines.map((line, i) => (\n                  <span key={i} className=\"flex min-w-max\">\n                    {lineNumbers && (\n                      <span aria-hidden=\"true\" className=\"me-4 w-5 shrink-0 text-end text-muted-foreground tabular-nums select-none\">\n                        {i + 1}\n                      </span>\n                    )}\n                    <span className=\"whitespace-pre\">\n                      {line.length === 0\n                        ? \" \"\n                        : line.map((t, j) => (\n                            <span key={j} className={tokenClass[t.type]}>\n                              {t.text}\n                            </span>\n                          ))}\n                    </span>\n                  </span>\n                ))}\n              </code>\n            </pre>\n          </TabsPrimitive.Content>\n        )\n      })}\n    </TabsPrimitive.Root>\n  )\n}\n\nexport { SnippetTabs, type SnippetTabsProps, type Snippet }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/snippet-tabs.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "code",
      "snippet",
      "tabs",
      "api",
      "curl"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "snippets is [{ label, code, language? }]. Language comes from the label (cURL, Python, Go, JavaScript). storageKey remembers the reader's pick and syncs every instance. variables replaces {{name}} in code and in what Copy puts on the clipboard.",
      "whenToUse": [
        "API docs that show one call in several languages",
        "Getting-started pages where code should use the viewer's own key"
      ],
      "whenNotToUse": [
        "Package manager commands (install-tabs)",
        "One file per tab (code-block tabs)"
      ],
      "composesWith": [
        "install-tabs",
        "code-block",
        "api-key-field",
        "api-endpoint"
      ],
      "a11y": [
        {
          "keys": "ArrowLeft / ArrowRight",
          "action": "Moves between languages"
        },
        {
          "keys": "Tab",
          "action": "Reaches Copy and the scrollable code, which is a named region"
        },
        {
          "keys": "Screen readers",
          "action": "Tab list named by the title; each panel is a labelled region"
        }
      ],
      "customization": [
        "storageKey",
        "variables",
        "lineNumbers",
        "title",
        "bodyClassName for a max height"
      ]
    },
    "examples": [
      "snippet-tabs-demo",
      "snippet-tabs-variables"
    ],
    "url": "https://ui.ballmac.com/components/snippet-tabs"
  },
  "categories": [
    "developer"
  ],
  "type": "registry:ui"
}