{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"copy-code-snippet","type":"registry:component","title":"Copy Code Snippet","description":"A dark code block for install commands: npm, pnpm, yarn and bun tabs, syntax-tinted tokens, line highlights, a copy button that really writes to the clipboard and only confirms when it did, and long lines that scroll under edge fades. No dependencies.","dependencies":[],"registryDependencies":[],"files":[{"path":"components/viberdy/copy-code-snippet.tsx","content":"\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport type { KeyboardEvent, PointerEvent } from \"react\";\n\ntype PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\ntype Tok = { t: string; c: string; i?: boolean };\n\nconst CSN_MANAGERS: readonly PackageManager[] = [\"npm\", \"pnpm\", \"yarn\", \"bun\"];\nconst CSN_SHELL = new Set([\"bash\", \"sh\", \"shell\", \"zsh\", \"terminal\", \"console\"]);\nconst CSN_HASH = new Set([\"py\", \"python\", \"yaml\", \"yml\", \"toml\", \"rb\", \"ruby\", \"env\"]);\nconst CSN_SUBS = new Set([\"add\", \"init\", \"install\", \"i\", \"run\", \"dlx\", \"exec\", \"create\", \"remove\", \"build\", \"dev\", \"start\", \"test\", \"update\"]);\nconst CSN_KEYWORDS = new Set([\n  \"const\", \"let\", \"var\", \"function\", \"return\", \"import\", \"from\", \"export\", \"default\", \"if\", \"else\", \"for\", \"while\",\n  \"async\", \"await\", \"new\", \"class\", \"extends\", \"type\", \"interface\", \"true\", \"false\", \"null\", \"undefined\", \"def\", \"in\", \"of\",\n]);\n\nconst CSN_COLOR = {\n  cmd: \"#c4b5fd\",\n  sub: \"#f4f4f5\",\n  flag: \"#7dd3fc\",\n  pkg: \"#fcd34d\",\n  str: \"#86efac\",\n  com: \"#6b7280\",\n  num: \"#fdba74\",\n  fn: \"#7dd3fc\",\n  key: \"#93c5fd\",\n  dim: \"#71717a\",\n  base: \"#e4e4e7\",\n};\n\n/** npm-flavoured lines rewritten for another package manager. */\nfunction csnConvert(line: string, pm: PackageManager): string {\n  if (pm === \"npm\") return line;\n  const m = line.match(/^(\\s*)(npx|npm)\\s+(.*)$/);\n  if (!m) return line;\n  const ind = m[1] ?? \"\";\n  const bin = m[2] ?? \"\";\n  const rest = m[3] ?? \"\";\n  if (bin === \"npx\") return ind + (pm === \"bun\" ? \"bunx\" : pm + \" dlx\") + \" \" + rest;\n  const add = rest.match(/^(?:install|i|add)(?:\\s+(.*))?$/);\n  if (add) return add[1] ? ind + pm + \" add \" + add[1] : ind + pm + \" install\";\n  const rm = rest.match(/^(?:uninstall|remove|rm)\\s+(.*)$/);\n  if (rm) return ind + pm + \" remove \" + (rm[1] ?? \"\");\n  return ind + pm + \" \" + rest;\n}\n\nfunction csnShell(line: string): Tok[] {\n  const out: Tok[] = [];\n  const re = /(#.*$)|(\"[^\"]*\"|'[^']*')|(\\s+)|(&&|\\|\\||[|;<>]+)|([^\\s\"'|;&<>]+)|(.)/g;\n  let expectCmd = true;\n  let afterCmd = false;\n  let m: RegExpExecArray | null;\n  while ((m = re.exec(line)) !== null) {\n    const [all, com, str, ws, op, word] = m;\n    if (com) out.push({ t: com, c: CSN_COLOR.com, i: true });\n    else if (str) out.push({ t: str, c: CSN_COLOR.str });\n    else if (ws) out.push({ t: ws, c: CSN_COLOR.base });\n    else if (op) {\n      out.push({ t: op, c: CSN_COLOR.dim });\n      expectCmd = !/[<>]/.test(op);\n      afterCmd = false;\n    } else if (word) {\n      if (expectCmd && /^[A-Za-z_]+=/.test(word)) {\n        out.push({ t: word, c: CSN_COLOR.dim });\n      } else if (expectCmd) {\n        out.push({ t: word, c: CSN_COLOR.cmd });\n        expectCmd = false;\n        afterCmd = true;\n      } else if (word.startsWith(\"-\")) {\n        out.push({ t: word, c: CSN_COLOR.flag });\n      } else {\n        if (afterCmd && CSN_SUBS.has(word)) out.push({ t: word, c: CSN_COLOR.sub });\n        else if (/[@/]/.test(word)) {\n          const at = word.lastIndexOf(\"@\");\n          if (at > 0) {\n            out.push({ t: word.slice(0, at), c: CSN_COLOR.pkg });\n            out.push({ t: word.slice(at), c: CSN_COLOR.dim });\n          } else out.push({ t: word, c: CSN_COLOR.pkg });\n        } else if (/^\\d/.test(word)) out.push({ t: word, c: CSN_COLOR.num });\n        else out.push({ t: word, c: CSN_COLOR.base });\n        afterCmd = false;\n      }\n    } else out.push({ t: all, c: CSN_COLOR.base });\n  }\n  return out;\n}\n\nfunction csnGeneric(line: string, hash: boolean): Tok[] {\n  const out: Tok[] = [];\n  const re = /(\\/\\/.*$)|(#.*$)|(\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'|`(?:[^`\\\\]|\\\\.)*`)|(\\b\\d+(?:\\.\\d+)?\\b)|([A-Za-z_$][\\w$]*)|(\\s+)|(.)/g;\n  let m: RegExpExecArray | null;\n  while ((m = re.exec(line)) !== null) {\n    const [all, slash, pound, str, num, id] = m;\n    const rest = line.slice(m.index + all.length);\n    if (slash) out.push({ t: slash, c: CSN_COLOR.com, i: true });\n    else if (pound) out.push(hash ? { t: pound, c: CSN_COLOR.com, i: true } : { t: pound, c: CSN_COLOR.base });\n    else if (str) out.push({ t: str, c: /^\\s*:/.test(rest) ? CSN_COLOR.key : CSN_COLOR.str });\n    else if (num) out.push({ t: num, c: CSN_COLOR.num });\n    else if (id) {\n      if (CSN_KEYWORDS.has(id)) out.push({ t: id, c: CSN_COLOR.cmd });\n      else if (rest.startsWith(\"(\")) out.push({ t: id, c: CSN_COLOR.fn });\n      else out.push({ t: id, c: CSN_COLOR.base });\n    } else out.push({ t: all, c: /^[^\\w\\s]$/.test(all) ? CSN_COLOR.dim : CSN_COLOR.base });\n  }\n  return out;\n}\n\nasync function csnWrite(text: string): Promise<boolean> {\n  try {\n    if (typeof navigator !== \"undefined\" && navigator.clipboard && typeof navigator.clipboard.writeText === \"function\") {\n      await navigator.clipboard.writeText(text);\n      return true;\n    }\n  } catch {\n    // Permission denied or insecure context: fall through to the legacy path.\n  }\n  if (typeof document === \"undefined\") return false;\n  const prev = document.activeElement as HTMLElement | null;\n  const ta = document.createElement(\"textarea\");\n  ta.value = text;\n  ta.setAttribute(\"readonly\", \"\");\n  ta.setAttribute(\"aria-hidden\", \"true\");\n  ta.style.cssText = \"position:fixed;top:-1000px;left:0;opacity:0;pointer-events:none;\";\n  document.body.appendChild(ta);\n  let ok = false;\n  try {\n    ta.select();\n    ta.setSelectionRange(0, text.length);\n    ok = document.execCommand(\"copy\");\n  } catch {\n    ok = false;\n  }\n  document.body.removeChild(ta);\n  if (prev && typeof prev.focus === \"function\") prev.focus({ preventScroll: true });\n  return ok;\n}\n\nconst CSN_CSS = [\n  \".csn{container-type:inline-size;position:relative;color:#e4e4e7;font-family:inherit;-webkit-tap-highlight-color:transparent}\",\n  \".csn ::selection{background:rgba(125,211,252,.28)}\",\n  \".csn-glow,.csn-ring{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .35s ease;border-radius:inherit}\",\n  \".csn-glow{background:radial-gradient(260px circle at var(--mx,50%) var(--my,0%),rgba(125,211,252,.09),transparent 70%)}\",\n  \".csn-ring{padding:1px;background:radial-gradient(180px circle at var(--mx,50%) var(--my,0%),rgba(186,230,253,.55),transparent 70%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box exclude,linear-gradient(#000 0 0)}\",\n  \".csn:hover .csn-glow,.csn:hover .csn-ring,.csn:focus-within .csn-glow,.csn:focus-within .csn-ring{opacity:1}\",\n  \".csn-head{position:relative;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 8px 8px 8px;border-bottom:1px solid rgba(255,255,255,.07)}\",\n  \".csn-tabs{position:relative;display:grid;grid-auto-flow:column;grid-auto-columns:1fr;padding:3px;border-radius:10px;background:rgba(255,255,255,.04);box-shadow:inset 0 0 0 1px rgba(255,255,255,.06)}\",\n  \".csn-pill{position:absolute;top:3px;bottom:3px;left:3px;border-radius:7px;background:rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.12),0 1px 2px rgba(0,0,0,.4);transition:transform .34s cubic-bezier(.34,1.32,.64,1)}\",\n  \".csn-t{position:relative;z-index:1;min-height:32px;padding:0 12px;border:0;border-radius:7px;background:transparent;color:#8b8d96;font:inherit;font-size:12.5px;font-weight:500;letter-spacing:.01em;cursor:pointer;transition:color .2s ease}\",\n  \".csn-t:hover{color:#d4d4d8}\",\n  \".csn-t[aria-selected=true]{color:#fff}\",\n  \".csn-lang{padding:3px 8px;border-radius:999px;background:rgba(255,255,255,.06);color:#9a9ca6;font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase}\",\n  \".csn-right{display:flex;align-items:center;gap:10px}\",\n  \".csn-copy{position:relative;display:inline-flex;align-items:center;gap:7px;min-height:32px;padding:0 12px 0 10px;border:1px solid rgba(255,255,255,.1);border-radius:9px;background:rgba(255,255,255,.05);color:#d4d4d8;font:inherit;font-size:12.5px;font-weight:500;cursor:pointer;transition:background .2s ease,border-color .25s ease,color .25s ease,transform .15s ease}\",\n  \".csn-copy:hover{background:rgba(255,255,255,.1);color:#fff}\",\n  \".csn-copy:active{transform:scale(.95)}\",\n  \".csn-copy[data-status=copied]{background:rgba(16,185,129,.14);border-color:rgba(52,211,153,.4);color:#6ee7b7}\",\n  \".csn-copy[data-status=failed]{background:rgba(244,63,94,.14);border-color:rgba(251,113,133,.4);color:#fda4af}\",\n  \".csn button:focus-visible{outline:2px solid #7dd3fc;outline-offset:2px}\",\n  \".csn-t:focus-visible{border-radius:7px}\",\n  \".csn-copy:focus-visible{border-radius:9px}\",\n  \".csn-stack{display:grid}\",\n  \".csn-stack>*{grid-area:1/1;transition:opacity .22s ease,transform .3s cubic-bezier(.2,.8,.2,1),filter .22s ease}\",\n  \".csn-icons{width:16px;height:16px;place-items:center}\",\n  \".csn-stack>[data-on=false]{opacity:0;transform:translateY(5px) scale(.7);filter:blur(2px)}\",\n  \".csn-labels>[data-on=false]{transform:translateY(7px)}\",\n  \".csn-stack>[data-on=true]{opacity:1;transform:none;filter:none}\",\n  \".csn-check{stroke-dasharray:22;stroke-dashoffset:22;transition:stroke-dashoffset .32s cubic-bezier(.65,0,.35,1) .08s}\",\n  \".csn-stack>[data-on=true] .csn-check{stroke-dashoffset:0}\",\n  \".csn-scroll{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none;-webkit-mask-image:linear-gradient(90deg,transparent 0,#000 var(--fl,0px),#000 calc(100% - var(--fr,0px)),transparent 100%);mask-image:linear-gradient(90deg,transparent 0,#000 var(--fl,0px),#000 calc(100% - var(--fr,0px)),transparent 100%)}\",\n  \".csn-scroll::-webkit-scrollbar{display:none}\",\n  \".csn-scroll:focus-visible{outline:2px solid #7dd3fc;outline-offset:-2px;border-radius:0}\",\n  \".csn-code{margin:0;padding:14px 0;width:max-content;min-width:100%;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace;font-size:13px;line-height:1.75;tab-size:2;animation:csn-in .28s cubic-bezier(.2,.8,.2,1) both}\",\n  \".csn-line{display:flex;align-items:baseline;padding:0 18px;white-space:pre;border-left:2px solid transparent;margin-left:0}\",\n  \".csn-line[data-hl=true]{background:linear-gradient(90deg,rgba(125,211,252,.12),rgba(125,211,252,.03));border-left-color:#7dd3fc}\",\n  \".csn-gut{flex:none;user-select:none;-webkit-user-select:none;color:#52525b;text-align:right;min-width:2ch;margin-right:16px}\",\n  \".csn-sweep{position:absolute;top:0;left:0;width:45%;height:1px;pointer-events:none;background:linear-gradient(90deg,transparent,#6ee7b7,transparent);box-shadow:0 0 14px 1px rgba(110,231,183,.55);animation:csn-sweep .85s cubic-bezier(.4,0,.2,1) both}\",\n  \".csn-wash{position:absolute;left:0;right:0;top:0;height:70%;pointer-events:none;background:linear-gradient(180deg,rgba(16,185,129,.12),transparent);animation:csn-wash 1s ease-out both}\",\n  \"@keyframes csn-in{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}\",\n  \"@keyframes csn-sweep{from{transform:translateX(-100%);opacity:1}80%{opacity:1}to{transform:translateX(230%);opacity:0}}\",\n  \"@keyframes csn-wash{from{opacity:1}to{opacity:0}}\",\n  \"@media (pointer:coarse){.csn-t,.csn-copy{min-height:40px}.csn-t{padding:0 13px}}\",\n  \"@container (max-width:380px){.csn-lang{display:none}.csn-t{padding:0 10px}}\",\n  \"@media (prefers-reduced-motion:reduce){.csn *,.csn *::before{animation:none!important;transition-duration:.01ms!important;transition-delay:0s!important}.csn-sweep,.csn-wash{display:none}.csn-stack>[data-on=false]{transform:none;filter:none}}\",\n].join(\"\\n\");\n\nexport function CopyCodeSnippet({\n  code = \"npx shadcn@latest add viberdy-dev/ui/copy-code-snippet\",\n  language = \"bash\",\n  packageTabs = true,\n  defaultTab = \"npm\",\n  lineNumbers,\n  highlight,\n  className = \"\",\n  onCopy,\n}: {\n  /** The snippet. Line breaks are kept exactly as given. */\n  code?: string;\n  /** Label pill and tokenizer hint (bash, ts, json, python ...). */\n  language?: string;\n  /** Show npm / pnpm / yarn / bun tabs when the snippet contains npx or npm lines. */\n  packageTabs?: boolean;\n  defaultTab?: PackageManager;\n  /** Gutter numbers. Left undefined: on for multi-line code that is not a shell command. */\n  lineNumbers?: boolean;\n  /** 1-based line numbers to mark. */\n  highlight?: number[];\n  className?: string;\n  onCopy?: (text: string) => void;\n}) {\n  const uid = useId();\n  const scroller = useRef<HTMLDivElement>(null);\n  const codeEl = useRef<HTMLPreElement>(null);\n  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const timer = useRef(0);\n  const [pm, setPm] = useState<PackageManager>(defaultTab);\n  const [status, setStatus] = useState<\"idle\" | \"copied\" | \"failed\">(\"idle\");\n  const [pulse, setPulse] = useState(0);\n\n  const lang = language.toLowerCase();\n  const shell = CSN_SHELL.has(lang);\n  const raw = code.replace(/\\r\\n/g, \"\\n\").replace(/\\n+$/, \"\");\n  const hasTabs = packageTabs && shell && /^\\s*(npx|npm)\\s/m.test(raw);\n  const active: PackageManager = hasTabs ? pm : \"npm\";\n  const lines = raw.split(\"\\n\").map((l) => (hasTabs ? csnConvert(l, active) : l));\n  const text = lines.join(\"\\n\");\n  const marked = new Set(highlight ?? []);\n  const numbered = lineNumbers ?? (!shell && lines.length > 1);\n  const tabIndex = Math.max(0, CSN_MANAGERS.indexOf(active));\n  const panelId = \"csn-panel-\" + uid.replace(/[^a-zA-Z0-9]/g, \"\");\n\n  useEffect(() => {\n    return () => window.clearTimeout(timer.current);\n  }, []);\n\n  // Edge fades follow how much code is hidden on each side.\n  useEffect(() => {\n    const el = scroller.current;\n    if (!el) return;\n    const update = () => {\n      const max = el.scrollWidth - el.clientWidth;\n      el.style.setProperty(\"--fl\", Math.min(36, Math.max(0, el.scrollLeft)) + \"px\");\n      el.style.setProperty(\"--fr\", Math.min(36, Math.max(0, max - el.scrollLeft)) + \"px\");\n      el.tabIndex = max > 1 ? 0 : -1;\n    };\n    update();\n    el.addEventListener(\"scroll\", update, { passive: true });\n    let ro: ResizeObserver | null = null;\n    if (typeof ResizeObserver !== \"undefined\") {\n      ro = new ResizeObserver(update);\n      ro.observe(el);\n      if (el.firstElementChild) ro.observe(el.firstElementChild);\n    }\n    return () => {\n      el.removeEventListener(\"scroll\", update);\n      ro?.disconnect();\n    };\n  }, [text, active]);\n\n  async function copy() {\n    const ok = await csnWrite(text);\n    window.clearTimeout(timer.current);\n    if (ok) {\n      setStatus(\"copied\");\n      setPulse((n) => n + 1);\n      onCopy?.(text);\n    } else {\n      setStatus(\"failed\");\n      const sel = window.getSelection();\n      if (sel && codeEl.current) {\n        const range = document.createRange();\n        range.selectNodeContents(codeEl.current);\n        sel.removeAllRanges();\n        sel.addRange(range);\n      }\n    }\n    timer.current = window.setTimeout(() => setStatus(\"idle\"), ok ? 1900 : 2600);\n  }\n\n  function onMove(e: PointerEvent<HTMLDivElement>) {\n    const el = e.currentTarget;\n    const r = el.getBoundingClientRect();\n    const k = r.width > 0 ? el.offsetWidth / r.width : 1;\n    el.style.setProperty(\"--mx\", (e.clientX - r.left) * k + \"px\");\n    el.style.setProperty(\"--my\", (e.clientY - r.top) * k + \"px\");\n  }\n\n  function onTabKey(e: KeyboardEvent<HTMLDivElement>) {\n    let next = tabIndex;\n    if (e.key === \"ArrowRight\") next = (tabIndex + 1) % CSN_MANAGERS.length;\n    else if (e.key === \"ArrowLeft\") next = (tabIndex - 1 + CSN_MANAGERS.length) % CSN_MANAGERS.length;\n    else if (e.key === \"Home\") next = 0;\n    else if (e.key === \"End\") next = CSN_MANAGERS.length - 1;\n    else return;\n    e.preventDefault();\n    const target = CSN_MANAGERS[next];\n    if (target) setPm(target);\n    tabRefs.current[next]?.focus();\n  }\n\n  const label = hasTabs ? \"Copy \" + active + \" command\" : \"Copy code\";\n\n  return (\n    <>\n      <style>{CSN_CSS}</style>\n      <div\n        onPointerMove={onMove}\n        className={\"csn w-full max-w-md overflow-hidden rounded-2xl \" + className}\n        style={{\n          background: \"linear-gradient(180deg,#101116,#0a0b0e)\",\n          boxShadow:\n            \"0 0 0 1px rgba(255,255,255,.09), inset 0 1px 0 rgba(255,255,255,.07), 0 1px 2px rgba(0,0,0,.25), 0 28px 60px -28px rgba(0,0,0,.65)\",\n        }}\n      >\n        <div aria-hidden className=\"csn-glow\" />\n        <div aria-hidden className=\"csn-ring\" />\n        {pulse > 0 && status === \"copied\" ? (\n          <div key={pulse} aria-hidden>\n            <div className=\"csn-wash\" />\n            <div className=\"csn-sweep\" />\n          </div>\n        ) : null}\n\n        <div className=\"csn-head\">\n          {hasTabs ? (\n            <div role=\"tablist\" aria-label=\"Package manager\" className=\"csn-tabs\" onKeyDown={onTabKey}>\n              <span\n                aria-hidden\n                className=\"csn-pill\"\n                style={{\n                  width: \"calc((100% - 6px) / \" + CSN_MANAGERS.length + \")\",\n                  transform: \"translateX(\" + tabIndex * 100 + \"%)\",\n                }}\n              />\n              {CSN_MANAGERS.map((m, i) => (\n                <button\n                  key={m}\n                  ref={(el) => {\n                    tabRefs.current[i] = el;\n                  }}\n                  type=\"button\"\n                  role=\"tab\"\n                  id={panelId + \"-t\" + m}\n                  aria-selected={m === active}\n                  aria-controls={panelId}\n                  tabIndex={m === active ? 0 : -1}\n                  className=\"csn-t\"\n                  onClick={() => setPm(m)}\n                >\n                  {m}\n                </button>\n              ))}\n            </div>\n          ) : (\n            <span className=\"csn-lang\">{language}</span>\n          )}\n          <div className=\"csn-right\">\n            {hasTabs ? <span className=\"csn-lang\">{language}</span> : null}\n            <button type=\"button\" className=\"csn-copy\" data-status={status} aria-label={label} onClick={copy}>\n              <span aria-hidden className=\"csn-stack csn-icons\">\n                <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" data-on={status === \"idle\"}>\n                  <rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8\" rx=\"2\" />\n                  <path d=\"M10.5 3.5v-.2A1.8 1.8 0 0 0 8.7 1.5H3.8A1.8 1.8 0 0 0 2 3.3v4.9a1.8 1.8 0 0 0 1.8 1.8h.2\" />\n                </svg>\n                <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.9\" strokeLinecap=\"round\" strokeLinejoin=\"round\" data-on={status === \"copied\"}>\n                  <path className=\"csn-check\" pathLength={22} d=\"M3 8.4l3.2 3.2L13 4.6\" />\n                </svg>\n                <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.9\" strokeLinecap=\"round\" data-on={status === \"failed\"}>\n                  <path d=\"M4 4l8 8M12 4l-8 8\" />\n                </svg>\n              </span>\n              <span aria-hidden className=\"csn-stack csn-labels\">\n                <span data-on={status === \"idle\"}>Copy</span>\n                <span data-on={status === \"copied\"}>Copied</span>\n                <span data-on={status === \"failed\"}>Failed</span>\n              </span>\n            </button>\n          </div>\n        </div>\n\n        <div id={panelId} role={hasTabs ? \"tabpanel\" : undefined} aria-labelledby={hasTabs ? panelId + \"-t\" + active : undefined}>\n          <div ref={scroller} className=\"csn-scroll\" role=\"region\" aria-label={language + \" code\"}>\n            <pre ref={codeEl} key={active} className=\"csn-code\">\n              {lines.map((line, i) => (\n                <span key={i} className=\"csn-line\" data-hl={marked.has(i + 1)}>\n                  {numbered ? (\n                    <span aria-hidden className=\"csn-gut\">\n                      {i + 1}\n                    </span>\n                  ) : shell && line.trim() !== \"\" && !line.trim().startsWith(\"#\") ? (\n                    <span aria-hidden className=\"csn-gut\" style={{ minWidth: \"1ch\", marginRight: 10 }}>\n                      $\n                    </span>\n                  ) : null}\n                  <code style={{ font: \"inherit\" }}>\n                    {(shell ? csnShell(line) : csnGeneric(line, CSN_HASH.has(lang))).map((tok, j) => (\n                      <span key={j} style={{ color: tok.c, fontStyle: tok.i ? \"italic\" : undefined }}>\n                        {tok.t}\n                      </span>\n                    ))}\n                    {line === \"\" ? \" \" : null}\n                  </code>\n                </span>\n              ))}\n            </pre>\n          </div>\n        </div>\n\n        <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n          {status === \"copied\" ? \"Copied to clipboard\" : status === \"failed\" ? \"Copy failed. The code is selected, press Ctrl+C.\" : \"\"}\n        </span>\n      </div>\n    </>\n  );\n}\n","type":"registry:component","target":"components/viberdy/copy-code-snippet.tsx"}],"meta":{"tier":"free","tags":["code","copy","clipboard","snippet","install","tabs","syntax"],"href":"https://viberdy.dev/library/copy-code-snippet","aiPrompt":"Create a dependency-free React + Tailwind component named CopyCodeSnippet (\"use client\", one file, no icon library, inline SVG only).\n\nProps (all optional): code = \"npx shadcn@latest add viberdy-dev/ui/copy-code-snippet\", language = \"bash\", packageTabs = true, defaultTab \"npm\" | \"pnpm\" | \"yarn\" | \"bun\" = \"npm\", lineNumbers (undefined = on for multi-line non-shell code), highlight (number[], 1-based lines), className, onCopy(text). Root: w-full max-w-md, rounded-2xl, overflow-hidden, own dark surface (linear-gradient #101116 to #0a0b0e, 1px white 9% ring as box-shadow, inset top highlight, soft drop shadow). Render the CSS in one plain <style> element beside the root in a fragment (React 18 and 19 safe); prefix classes csn-; the root is container-type inline-size.\n\nHeader (8px padding, 1px bottom rule at 7% white): left, when the code contains npx/npm lines and language is a shell (bash, sh, shell, zsh, terminal, console) and packageTabs is true, a tablist of npm / pnpm / yarn / bun (equal-width grid, 3px padding, 10px radius, 4% white well). A pill (10% white, inset highlight) sits behind the active tab and slides with translateX(index * 100%) over 0.34s cubic-bezier(.34,1.32,.64,1). Otherwise the left slot is the language pill (10.5px uppercase, 0.08em tracking). Arrow Left/Right/Home/End move the tab and focus (roving tabindex, role=tablist/tab/tabpanel, aria-selected, aria-controls). Right: the language pill (hidden when the root is under 380px wide) and the Copy button.\n\nTab conversion of each line: npx X becomes \"pnpm dlx X\", \"yarn dlx X\" or \"bunx X\"; npm install/i/add X becomes \"<pm> add X\" (bare npm install stays \"<pm> install\"); npm uninstall/remove becomes \"<pm> remove\"; other npm lines swap the binary. The shown and the COPIED text always match the active tab.\n\nCopy: async. Try navigator.clipboard.writeText; on a missing API or a rejection fall back to a hidden readonly textarea (position fixed, off-screen) + select + document.execCommand(\"copy\"), restoring the previously focused element. Status is idle | copied | failed and \"copied\" is ONLY set when a copy really succeeded. On failure select the code text with a Range so Ctrl+C works. Revert to idle after 1.9s (2.6s for failed); clear the timer on unmount. Button: 32px tall (40px on pointer:coarse), 9px radius, 5% white fill, 1px 10% white border, 12.5px/500 text; hover 10% fill; active scale .95; copied = emerald (#6ee7b7 text, 14% emerald fill, 40% border); failed = rose. Content is a 16px icon stack (copy glyph, check drawn with stroke-dasharray 22 to 0 in 0.32s with 0.08s delay, cross) and a label stack (Copy / Copied / Failed) in one grid cell, inactive layers at opacity 0, translateY 5-7px, scale .7, blur 2px, transitions 0.22-0.3s cubic-bezier(.2,.8,.2,1), so the width never jumps. aria-label \"Copy code\" / \"Copy <pm> command\" with the visible content aria-hidden; a visually hidden role=status live region announces \"Copied to clipboard\" or the failure. On copied: a 1px emerald line with a glow sweeps across the top edge (translateX -100% to 230% over 0.85s) and an emerald wash fades from the top over 1s (remount keyed by a pulse counter).\n\nBody: a scroll container (overflow-x auto, scrollbar hidden, role=region, aria-label \"<language> code\", tabIndex set to 0 only while it overflows) with a mask-image of linear-gradient(90deg, transparent, #000 var(--fl), #000 calc(100% - var(--fr)), transparent) where --fl = min(36, scrollLeft) px and --fr = min(36, remaining scroll) px, updated on scroll and by a ResizeObserver. Inside: <pre> (ui-monospace, 13px/1.75, width max-content, min-width 100%, 14px vertical padding) animating in with a 0.28s 5px rise whenever the tab changes (key on the tab). One flex span per line (18px side padding, white-space pre, 2px transparent left border). Highlighted lines: left border #7dd3fc and a 12% to 3% sky gradient. Gutter: line numbers (#52525b, 2ch, right aligned, aria-hidden, user-select none) or, for shell code without numbers, a dim \"$\" on command lines.\n\nTokenizer (no library): shell lines tint the command (#c4b5fd), subcommands (add, init, install, run, dlx ... #f4f4f5), flags (#7dd3fc), package names (anything with @ or /; #fcd34d, with a dim @version suffix), strings (#86efac), comments (#6b7280 italic), numbers (#fdba74); operators && || | ; reset the \"expect command\" state. Other languages tint strings (JSON keys #93c5fd), // and (python/yaml/toml/ruby) # comments, numbers, keywords (#c4b5fd) and identifiers followed by \"(\" (#7dd3fc).\n\nPointer: pointermove maps to layout pixels ((clientX - rect.left) * offsetWidth / rect.width) into --mx/--my; a radial sky glow and a 1px masked ring (mask-composite exclude) fade in on hover or focus-within. Focus: 2px #7dd3fc outlines, offset 2px on buttons, inset on the scroller. prefers-reduced-motion: animations off, transitions near-instant, sweep and wash hidden.","license":"https://viberdy.dev/license"}}