{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-export",
  "title": "Chart Export",
  "description": "BoldKit chart export helpers — framework-agnostic, SSR-safe CSV / PNG / SVG download and fullscreen toggle. Works with any chart engine that renders an <svg> (Recharts) or <canvas> (ECharts).",
  "files": [
    {
      "path": "registry/default/lib/chart-export.ts",
      "content": "/**\n * BoldKit Chart Export — framework-agnostic chart toolbar helpers.\n *\n * Operates on a chart's container element, so it works regardless of the\n * rendering engine: Recharts (SVG) and ECharts (canvas) are both handled.\n * Consumed by the React <ChartToolbar> and the Vue <ChartToolbar>.\n *\n * All functions are SSR-safe: they no-op on the server.\n */\n\nconst isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'\n\nfunction triggerDownload(href: string, filename: string) {\n  const a = document.createElement('a')\n  a.href = href\n  a.download = filename\n  document.body.appendChild(a)\n  a.click()\n  a.remove()\n}\n\n// ──────────────────────────────────────────────────────────────────\n// CSV — from the same data array you feed the chart\n// ──────────────────────────────────────────────────────────────────\n\n/** Escape a single CSV cell per RFC 4180 (quote if it contains ,\"\\n). */\nfunction csvCell(value: unknown): string {\n  const s = value == null ? '' : String(value)\n  return /[\",\\n]/.test(s) ? `\"${s.replace(/\"/g, '\"\"')}\"` : s\n}\n\n/**\n * Serialize an array of row objects to a CSV string. Columns are the union\n * of keys across all rows, in first-seen order. Pure — no DOM access.\n */\nexport function toCSV(rows: Array<Record<string, unknown>>): string {\n  if (!rows.length) return ''\n\n  const columns: string[] = []\n  for (const row of rows) {\n    for (const key of Object.keys(row)) {\n      if (!columns.includes(key)) columns.push(key)\n    }\n  }\n\n  const lines = [\n    columns.map(csvCell).join(','),\n    ...rows.map((row) => columns.map((col) => csvCell(row[col])).join(',')),\n  ]\n  return lines.join('\\n')\n}\n\n/**\n * Serialize an array of row objects to CSV and download it.\n * Columns are the union of keys across all rows, in first-seen order.\n */\nexport function downloadCSV(rows: Array<Record<string, unknown>>, filename = 'chart.csv'): void {\n  if (!isBrowser || !rows.length) return\n\n  const blob = new Blob([toCSV(rows)], { type: 'text/csv;charset=utf-8;' })\n  const url = URL.createObjectURL(blob)\n  triggerDownload(url, filename)\n  URL.revokeObjectURL(url)\n}\n\n// ──────────────────────────────────────────────────────────────────\n// SVG — only when the engine renders vector output (Recharts)\n// ──────────────────────────────────────────────────────────────────\n\n// The container holds the toolbar buttons (whose icons are <svg> elements)\n// AND the chart. Skip any <svg> belonging to the toolbar controls so we\n// serialize the chart, not a button icon.\nfunction findSvg(container: HTMLElement): SVGSVGElement | null {\n  for (const svg of container.querySelectorAll('svg')) {\n    if (!svg.closest('[data-chart-export-controls]')) return svg as SVGSVGElement\n  }\n  return null\n}\n\n/** Returns true when the container has an <svg> to export (Recharts). */\nexport function canExportSVG(container: HTMLElement | null): boolean {\n  return !!container && !!findSvg(container)\n}\n\n// Presentation properties that carry a chart's appearance. Recharts drives\n// most of these through CSS variables (e.g. fill: hsl(var(--primary))) and\n// external stylesheets — neither of which survive serialization. We read the\n// *computed* value (which resolves the variables to concrete colors) and pin\n// it on the clone so the exported SVG is fully self-contained.\n//\n// All of these are valid SVG presentation attributes, so we write them BOTH\n// as an inline style AND as attributes — overwriting any `fill=\"hsl(var(--x))\"`\n// left on the element. Chrome honours the style override, but Preview /\n// Quick Look / Illustrator and many rasterizers read the attribute and choke\n// on var(), rendering the chart colorless. Overwriting the attribute fixes\n// color in every viewer.\nconst SVG_STYLE_PROPS = [\n  'fill', 'fill-opacity', 'fill-rule',\n  'stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray',\n  'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit',\n  'opacity', 'color', 'visibility', 'display',\n  'font-family', 'font-size', 'font-weight', 'font-style',\n  'text-anchor', 'dominant-baseline', 'letter-spacing',\n]\n\nfunction inlineComputedStyles(src: Element, dst: Element): void {\n  const cs = getComputedStyle(src)\n  let style = ''\n  for (const prop of SVG_STYLE_PROPS) {\n    const value = cs.getPropertyValue(prop)\n    if (!value) continue\n    style += `${prop}:${value};`\n    // Overwrite the presentation attribute with the concrete value so\n    // renderers that don't apply CSS `style` precedence still get color.\n    dst.setAttribute(prop, value)\n  }\n  dst.setAttribute('style', style)\n\n  const srcChildren = src.children\n  const dstChildren = dst.children\n  for (let i = 0; i < srcChildren.length; i++) {\n    if (dstChildren[i]) inlineComputedStyles(srcChildren[i], dstChildren[i])\n  }\n}\n\n/**\n * Clone the live <svg>, inline every element's computed style so it renders\n * standalone, and return the serialized markup plus its rendered size.\n */\nfunction serializeSvg(svg: SVGSVGElement): { source: string; width: number; height: number } {\n  const rect = svg.getBoundingClientRect()\n  const width = Math.round(rect.width || svg.clientWidth || 640)\n  const height = Math.round(rect.height || svg.clientHeight || 320)\n\n  const clone = svg.cloneNode(true) as SVGSVGElement\n  inlineComputedStyles(svg, clone)\n  clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')\n  clone.setAttribute('width', String(width))\n  clone.setAttribute('height', String(height))\n  if (!clone.getAttribute('viewBox')) {\n    clone.setAttribute('viewBox', `0 0 ${width} ${height}`)\n  }\n  // Opaque backdrop so the export isn't transparent (reads as black in some viewers).\n  const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect')\n  bg.setAttribute('x', '0')\n  bg.setAttribute('y', '0')\n  bg.setAttribute('width', String(width))\n  bg.setAttribute('height', String(height))\n  bg.setAttribute('fill', '#ffffff')\n  clone.insertBefore(bg, clone.firstChild)\n\n  return { source: new XMLSerializer().serializeToString(clone), width, height }\n}\n\n/** Serialize the container's <svg> and download it. No-op if there's no svg. */\nexport function exportSVG(container: HTMLElement, filename = 'chart.svg'): void {\n  if (!isBrowser) return\n  const svg = findSvg(container)\n  if (!svg) return\n  const { source } = serializeSvg(svg)\n  const blob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' })\n  const url = URL.createObjectURL(blob)\n  triggerDownload(url, filename)\n  URL.revokeObjectURL(url)\n}\n\n// ──────────────────────────────────────────────────────────────────\n// PNG — rasterize an <svg>, or grab an existing <canvas> directly\n// ──────────────────────────────────────────────────────────────────\n\n/**\n * Export the chart as a PNG. Handles both SVG (Recharts) and canvas\n * (ECharts) engines. `scale` multiplies resolution for crisp exports.\n */\nexport async function exportPNG(\n  container: HTMLElement,\n  filename = 'chart.png',\n  scale = 2\n): Promise<void> {\n  if (!isBrowser) return\n\n  // Canvas engine (ECharts) — composite the bitmap onto white so a\n  // transparent chart background doesn't export as black.\n  const canvasEl = container.querySelector('canvas')\n  if (canvasEl) {\n    const out = document.createElement('canvas')\n    out.width = canvasEl.width\n    out.height = canvasEl.height\n    const c = out.getContext('2d')\n    if (!c) return\n    c.fillStyle = '#ffffff'\n    c.fillRect(0, 0, out.width, out.height)\n    c.drawImage(canvasEl, 0, 0)\n    triggerDownload(out.toDataURL('image/png'), filename)\n    return\n  }\n\n  // SVG engine (Recharts) — draw the style-inlined vector onto an offscreen canvas.\n  const svg = findSvg(container)\n  if (!svg) return\n\n  const { source, width, height } = serializeSvg(svg)\n  const svgUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source)\n\n  await new Promise<void>((resolve) => {\n    const img = new Image()\n    img.onload = () => {\n      const canvas = document.createElement('canvas')\n      canvas.width = width * scale\n      canvas.height = height * scale\n      const ctx = canvas.getContext('2d')\n      if (!ctx) return resolve()\n      // White backdrop so transparent charts don't render black on export.\n      ctx.fillStyle = '#ffffff'\n      ctx.fillRect(0, 0, canvas.width, canvas.height)\n      ctx.drawImage(img, 0, 0, canvas.width, canvas.height)\n      triggerDownload(canvas.toDataURL('image/png'), filename)\n      resolve()\n    }\n    img.onerror = () => resolve()\n    img.src = svgUrl\n  })\n}\n\n// ──────────────────────────────────────────────────────────────────\n// Fullscreen — toggle the chart container into fullscreen\n// ──────────────────────────────────────────────────────────────────\n\n/**\n * Toggle the container in/out of fullscreen. Safe no-op where unsupported.\n *\n * A fullscreened element is sized to the whole screen, but the chart inside\n * keeps its fixed height — leaving the browser's black backdrop showing in the\n * gap. On enter we give the container an opaque background and stretch the\n * chart to fill; on exit we restore the original inline styles.\n */\nexport function toggleFullscreen(container: HTMLElement): void {\n  if (!isBrowser) return\n\n  if (document.fullscreenElement) {\n    void document.exitFullscreen?.()\n    return\n  }\n  if (!container.requestFullscreen) return\n\n  // The chart is the last child (the toolbar is an absolutely-positioned sibling).\n  const chart = container.lastElementChild as HTMLElement | null\n  const prevContainerStyle = container.getAttribute('style') ?? ''\n  const prevChartStyle = chart?.getAttribute('style') ?? ''\n\n  const onChange = () => {\n    if (document.fullscreenElement === container) {\n      // Fill the screen with the page background and CENTER the chart, so the\n      // browser's black fullscreen backdrop never shows. Canvas engines\n      // (ECharts autoresize) grow to fill; SVG engines (Recharts) that don't\n      // re-measure simply sit centered on the matching background — no black gap.\n      container.style.background = 'hsl(var(--background, 0 0% 100%))'\n      container.style.boxSizing = 'border-box'\n      container.style.padding = '1.5rem'\n      container.style.display = 'flex'\n      container.style.flexDirection = 'column'\n      if (chart) {\n        // flex:1 + min-height:0 gives the chart a DEFINITE height (not a\n        // percentage), which is what Recharts' ResponsiveContainer and\n        // ECharts' autoresize need to grow. align-items:center + height:100%\n        // would instead collapse the chart to min-content. margin:auto centers.\n        chart.style.flex = '1 1 auto'\n        chart.style.minHeight = '0'\n        chart.style.width = '100%'\n        chart.style.maxWidth = '1600px'\n        chart.style.margin = '0 auto'\n      }\n    } else {\n      // Restore and detach — fullscreen was exited.\n      container.setAttribute('style', prevContainerStyle)\n      if (chart) chart.setAttribute('style', prevChartStyle)\n      document.removeEventListener('fullscreenchange', onChange)\n    }\n  }\n\n  document.addEventListener('fullscreenchange', onChange)\n  void container.requestFullscreen()\n}\n",
      "type": "registry:lib",
      "target": "lib/chart-export.ts"
    }
  ],
  "type": "registry:lib"
}