{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "install-tabs",
  "title": "Install Tabs",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Package-manager tabs for a CLI command (pnpm, npm, yarn, bun) with a copy button. Remembers the reader's choice and keeps every instance on the page in sync.",
  "dependencies": [
    "lucide-react",
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/install-tabs.tsx",
      "content": "// Ballmac UI: Install Tabs. https://ui.ballmac.com/components/install-tabs\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Copy } from \"lucide-react\"\nimport { Tabs as TabsPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype PackageManager = \"pnpm\" | \"npm\" | \"yarn\" | \"bun\"\n\nconst PACKAGE_MANAGERS: PackageManager[] = [\"pnpm\", \"npm\", \"yarn\", \"bun\"]\n\n/** How each manager runs a package without installing it. */\nconst RUNNERS: Record<PackageManager, string> = {\n  pnpm: \"pnpm dlx\",\n  npm: \"npx\",\n  yarn: \"yarn dlx\",\n  bun: \"bunx --bun\",\n}\n\nconst isManager = (value: unknown): value is PackageManager =>\n  typeof value === \"string\" && (PACKAGE_MANAGERS as string[]).includes(value)\n\n/* -------------------------------------------------------------------------------------------------\n * Shared, SSR-safe preference store: localStorage when available, memory otherwise.\n * Instances with the same storageKey stay in sync, including across browser tabs.\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 blocked (private mode, sandboxed iframe): 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\nconst noopSubscribe = () => () => {}\n\nfunction useStoredManager(key: string | undefined) {\n  const subscribe = React.useCallback(\n    (notify: () => void) => (key ? subscribeStored(key, notify) : () => {}),\n    [key]\n  )\n  const stored = React.useSyncExternalStore(\n    key ? subscribe : noopSubscribe,\n    () => (key ? readStored(key) : null),\n    // The server (and hydration) never sees storage, so markup always matches.\n    () => null\n  )\n  return isManager(stored) ? stored : null\n}\n\n/* -------------------------------------------------------------------------------------------------\n * InstallTabs\n * -----------------------------------------------------------------------------------------------*/\n\ntype InstallTabsProps = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"dir\"> & {\n  /** Command to run through each manager's runner, e.g. \"shadcn@latest add @ballmac/button\". */\n  command?: string\n  /** Explicit command per manager; overrides `command`. Only the managers listed get a tab. */\n  commands?: Partial<Record<PackageManager, string>>\n  /** Tab selected before any stored preference is read. */\n  defaultValue?: PackageManager\n  /** Controlled selected manager. */\n  value?: PackageManager\n  /** Called when the reader picks another manager. */\n  onValueChange?: (manager: PackageManager) => void\n  /** Remember the choice in localStorage under this key and sync every instance using it. */\n  storageKey?: string\n}\n\nfunction InstallTabs({\n  command,\n  commands,\n  defaultValue = \"pnpm\",\n  value: valueProp,\n  onValueChange,\n  storageKey,\n  className,\n  ...props\n}: InstallTabsProps) {\n  const msg = useMessages()\n  const entries = React.useMemo(() => {\n    if (commands) {\n      return PACKAGE_MANAGERS.flatMap((m) => (commands[m] ? [[m, commands[m]] as [PackageManager, string]] : []))\n    }\n    return PACKAGE_MANAGERS.map((m) => [m, command ? `${RUNNERS[m]} ${command}` : \"\"] as [PackageManager, string])\n  }, [command, commands])\n\n  const stored = useStoredManager(storageKey)\n  const [internal, setInternal] = React.useState<PackageManager>(defaultValue)\n  const wanted = valueProp ?? stored ?? internal\n  const available = entries.map(([m]) => m)\n  const value = available.includes(wanted) ? wanted : (available[0] ?? defaultValue)\n  const current = entries.find(([m]) => m === value)?.[1] ?? \"\"\n\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"install-tabs\"\n      value={value}\n      onValueChange={(next) => {\n        if (!isManager(next)) return\n        setInternal(next)\n        if (storageKey) writeStored(storageKey, next)\n        onValueChange?.(next)\n      }}\n      className={cn(\"w-full min-w-0 overflow-hidden rounded-xl border bg-card text-card-foreground\", className)}\n      {...props}\n    >\n      <div className=\"flex h-10 items-center gap-2 border-b bg-muted/40 pe-1.5 ps-1.5\">\n        <TabsPrimitive.List aria-label={msg(\"install-tabs.packageManager\", \"Package manager\")} className=\"flex min-w-0 items-center overflow-x-auto [scrollbar-width:none]\">\n          {entries.map(([manager]) => (\n            <TabsPrimitive.Trigger\n              key={manager}\n              value={manager}\n              className=\"relative h-10 shrink-0 px-2.5 font-mono text-xs text-muted-foreground outline-none transition-colors duration-150 after:absolute after:inset-x-2.5 after:bottom-0 after:h-px after:bg-transparent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset data-[state=active]:text-foreground data-[state=active]:after:bg-foreground\"\n            >\n              {manager}\n            </TabsPrimitive.Trigger>\n          ))}\n        </TabsPrimitive.List>\n        <CopyCommandButton value={current} />\n      </div>\n      {entries.map(([manager, cmd]) => (\n        <TabsPrimitive.Content\n          key={manager}\n          value={manager}\n          className=\"overflow-x-auto px-4 py-3 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset\"\n        >\n          <code dir=\"ltr\" data-slot=\"install-tabs-command\" className=\"font-mono text-[13px] leading-6 whitespace-pre\">\n            <span aria-hidden=\"true\" className=\"me-2 text-muted-foreground select-none\">\n              $\n            </span>\n            {cmd}\n          </code>\n        </TabsPrimitive.Content>\n      ))}\n    </TabsPrimitive.Root>\n  )\n}\n\nfunction CopyCommandButton({ value }: { value: string }) {\n  const msg = useMessages()\n  const [copied, setCopied] = React.useState(false)\n  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)\n  React.useEffect(() => () => clearTimeout(timer.current), [])\n  return (\n    <>\n      <button\n        type=\"button\"\n        data-slot=\"install-tabs-copy\"\n        aria-label={copied ? msg(\"install-tabs.copied\", \"Copied\") : msg(\"install-tabs.copyCommand\", \"Copy command\")}\n        title={msg(\"install-tabs.copyCommand\", \"Copy command\")}\n        onClick={async () => {\n          try {\n            await navigator.clipboard.writeText(value)\n          } catch {\n            return\n          }\n          setCopied(true)\n          clearTimeout(timer.current)\n          timer.current = setTimeout(() => setCopied(false), 1800)\n        }}\n        className=\"ms-auto flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors duration-150 hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n      >\n        {copied ? <Check aria-hidden=\"true\" className=\"size-3.5\" /> : <Copy aria-hidden=\"true\" className=\"size-3.5\" />}\n      </button>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {copied ? \"Copied to clipboard\" : \"\"}\n      </span>\n    </>\n  )\n}\n\nexport { InstallTabs, RUNNERS as packageManagerRunners, type InstallTabsProps, type PackageManager }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/install-tabs.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "install",
      "cli",
      "npm",
      "pnpm",
      "yarn",
      "bun",
      "tabs",
      "copy",
      "docs"
    ],
    "version": "1.0.0",
    "updated": "2026-09-28",
    "ai": {
      "summary": "<InstallTabs command='shadcn@latest add @ballmac/button' storageKey='pm' /> renders pnpm dlx / npx / yarn dlx / bunx --bun variants. Use commands={{ npm: 'npm i x', pnpm: 'pnpm add x' }} for install (not run) commands.",
      "whenToUse": [
        "Install or 'run this CLI' instructions in docs and READMEs",
        "Any page with several install snippets where the reader's package manager should stick"
      ],
      "whenNotToUse": [
        "A command with its output (use terminal)",
        "Multi-line source code (use code-block)"
      ],
      "composesWith": [
        "code-block",
        "terminal"
      ],
      "a11y": [
        {
          "keys": "← / →",
          "action": "Moves between package-manager tabs and selects them"
        },
        {
          "keys": "Home / End",
          "action": "Jumps to the first or last tab"
        },
        {
          "keys": "Tab",
          "action": "Moves to the copy button and the command panel"
        }
      ],
      "customization": [
        "command: run through each manager's runner; commands: explicit per-manager strings (only those get tabs)",
        "storageKey: remember the choice in localStorage, read after hydration (SSR-safe) and synced across instances and browser tabs",
        "value/onValueChange for controlled use; defaultValue (pnpm by default)"
      ]
    },
    "examples": [
      "install-tabs-demo"
    ],
    "url": "https://ui.ballmac.com/components/install-tabs"
  },
  "categories": [
    "developer"
  ],
  "type": "registry:ui"
}