{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "echarts-brush",
  "dependencies": [
    "echarts"
  ],
  "registryDependencies": [
    "@evilcharts/echarts-chart"
  ],
  "files": [
    {
      "path": "src/registry/ui/echarts-brush.tsx",
      "content": "import { withAlpha, type ResolvedColors } from \"@/registry/ui/echarts-chart\";\nimport type { DataZoomComponentOption } from \"echarts/components\";\nimport type { FC } from \"react\";\nimport * as echarts from \"echarts/core\";\n\ntype EChartsInstance = ReturnType<typeof echarts.init>;\n\nconst BRUSH_BORDER_OPACITY = 1; // brush frame, × border alpha (evil-brush uses the full token)\n\nexport { BRUSH_BORDER_OPACITY };\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Brush marker — the declarative `<Chart.Brush/>` child. Rendering nothing, its\n// PRESENCE turns the brush on (replacing the old showBrush prop) and its props\n// carry the brush's height, handle-label formatter, and range callback. Shared\n// so every cartesian chart attaches the SAME component to its root.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface BrushProps {\n  height?: number; // brush preview strip height in px (default 56)\n  formatLabel?: (value: string, index: number) => string; // formats the range-handle labels\n  onChange?: (range: { startIndex: number; endIndex: number }) => void; // fires as the range moves\n}\n\n/** Declares the zoom brush below the chart. Presence renders it; renders nothing itself. */\nexport const Brush: FC<BrushProps> = () => null;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Brush overlays — the evil-brush look: a rounded border around the SELECTED\n// range, dimmed unselected sides, centered grip-dot handle pills, and range\n// label pills below the frame. None of that is a dataZoom capability. They are\n// raw zrender elements updated imperatively — routing them through setOption\n// re-renders the dataZoom component mid-drag, resetting its drag anchor (the\n// handle progressively lags the pointer).\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type BrushRange = { start: number; end: number };\nexport type BrushGeometry = { bottom: number; height: number };\n\nexport type BrushOverlayParams = {\n  range: BrushRange;\n  geom: BrushGeometry;\n  size: { width: number; height: number };\n  tokens: ResolvedColors[\"tokens\"];\n  labels: { start: string; end: string } | null;\n  showLabels: boolean;\n  hover: { left: boolean; right: boolean };\n};\n\ntype ZrRect = InstanceType<typeof echarts.graphic.Rect>;\ntype ZrCircle = InstanceType<typeof echarts.graphic.Circle>;\ntype ZrText = InstanceType<typeof echarts.graphic.Text>;\n\nexport type BrushOverlayElements = {\n  dimLeft: ZrRect;\n  dimRight: ZrRect;\n  frame: ZrRect;\n  pillLeft: ZrRect;\n  pillRight: ZrRect;\n  grips: ZrCircle[]; // 3 left + 3 right\n  labelStart: ZrText;\n  labelEnd: ZrText;\n};\n\nexport function syncBrushOverlay(\n  chart: EChartsInstance,\n  store: { brushOverlay: BrushOverlayElements | null },\n  params: BrushOverlayParams | null,\n) {\n  const zr = chart.getZr();\n  if (!zr) return;\n\n  if (!params) {\n    if (store.brushOverlay) {\n      const { grips, ...rest } = store.brushOverlay;\n      [...Object.values(rest), ...grips].forEach((el) => zr.remove(el));\n      store.brushOverlay = null;\n    }\n    return;\n  }\n\n  if (!store.brushOverlay) {\n    const rect = (z: number) => new echarts.graphic.Rect({ silent: true, z, shape: {} });\n    const els: BrushOverlayElements = {\n      dimLeft: rect(100),\n      dimRight: rect(100),\n      frame: rect(101),\n      pillLeft: rect(102),\n      pillRight: rect(102),\n      grips: Array.from(\n        { length: 6 },\n        () => new echarts.graphic.Circle({ silent: true, z: 103, shape: {} }),\n      ),\n      labelStart: new echarts.graphic.Text({ silent: true, z: 104 }),\n      labelEnd: new echarts.graphic.Text({ silent: true, z: 104 }),\n    };\n    const { grips, ...rest } = els;\n    [...Object.values(rest), ...grips].forEach((el) => zr.add(el));\n    store.brushOverlay = els;\n  }\n\n  const els = store.brushOverlay;\n  const { range, geom, size, tokens, labels, showLabels, hover } = params;\n\n  const trackLeft = 8;\n  const trackRight = Math.max(size.width - 8, trackLeft);\n  const trackWidth = trackRight - trackLeft;\n  const top = size.height - geom.bottom - geom.height;\n  const centerY = top + geom.height / 2;\n  const selectionLeft = trackLeft + (trackWidth * range.start) / 100;\n  const selectionRight = trackLeft + (trackWidth * range.end) / 100;\n\n  const dimFill = withAlpha(tokens.background, 0.7);\n  els.dimLeft.setShape({\n    x: trackLeft,\n    y: top,\n    width: Math.max(selectionLeft - trackLeft, 0),\n    height: geom.height,\n  });\n  els.dimLeft.setStyle({ fill: dimFill });\n  els.dimRight.setShape({\n    x: selectionRight,\n    y: top,\n    width: Math.max(trackRight - selectionRight, 0),\n    height: geom.height,\n  });\n  els.dimRight.setStyle({ fill: dimFill });\n\n  els.frame.setShape({\n    x: selectionLeft,\n    y: top,\n    width: Math.max(selectionRight - selectionLeft, 0),\n    height: geom.height,\n    r: 6,\n  });\n  els.frame.setStyle({\n    fill: \"none\",\n    stroke: withAlpha(tokens.border, BRUSH_BORDER_OPACITY),\n    lineWidth: 1,\n  });\n\n  // Handle pills: evil-brush's 6×16 grip pill, centered on the selection edge,\n  // brightening to foreground on hover/drag.\n  const pill = (el: ZrRect, x: number, hovered: boolean) => {\n    el.setShape({ x: x - 3, y: centerY - 8, width: 6, height: 16, r: 3 });\n    el.setStyle({ fill: hovered ? tokens.foreground : tokens.mutedForeground });\n  };\n  pill(els.pillLeft, selectionLeft, hover.left);\n  pill(els.pillRight, selectionRight, hover.right);\n\n  const gripFill = withAlpha(tokens.background, 0.7);\n  [-4, 0, 4].forEach((offset, i) => {\n    els.grips[i].setShape({ cx: selectionLeft, cy: centerY + offset, r: 1 });\n    els.grips[i].setStyle({ fill: gripFill });\n    els.grips[i + 3].setShape({ cx: selectionRight, cy: centerY + offset, r: 1 });\n    els.grips[i + 3].setStyle({ fill: gripFill });\n  });\n\n  // Range label pills straddle the frame's bottom line — an overlay, so they\n  // occupy no layout space; half the pill sits above the line, half below. Each\n  // pill grows INWARD from its handle with a small inset, like the Recharts\n  // labels, instead of hanging past the frame edge.\n  const label = (el: ZrText, text: string, x: number, align: \"left\" | \"right\") => {\n    el.setStyle({\n      text,\n      x: align === \"left\" ? Math.max(x + 6, trackLeft + 2) : Math.min(x - 6, trackRight - 2),\n      y: top + geom.height,\n      align,\n      verticalAlign: \"middle\",\n      fill: tokens.background,\n      backgroundColor: tokens.foreground,\n      padding: [2, 5],\n      borderRadius: 4,\n      font: \"500 9px system-ui, sans-serif\",\n    });\n    el.attr(\"invisible\", !showLabels || !text);\n  };\n  label(els.labelStart, labels?.start ?? \"\", selectionLeft, \"left\");\n  label(els.labelEnd, labels?.end ?? \"\", selectionRight, \"right\");\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// dataZoom slider — the transparent drag layer laid over the mini chart. Fully\n// chart-agnostic: the visible frame/handles/labels are the graphic overlays\n// above; this provides interaction only. Both zoom entries target only the MAIN\n// x-axis (index 0), so the mini chart never filters itself. The per-chart\n// mini-series (which differ per chart type) are built by the chart, not here.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function buildBrushDataZoom(params: {\n  brushBottom: number;\n  brushHeight: number;\n  brushRange: BrushRange;\n  fillerColor: string;\n}): DataZoomComponentOption[] {\n  const { brushBottom, brushHeight, brushRange, fillerColor } = params;\n\n  return [\n    {\n      type: \"slider\",\n      show: true,\n      xAxisIndex: [0],\n      left: 8,\n      right: 8,\n      bottom: brushBottom,\n      height: brushHeight,\n      // Carry the live range through every rebuild — a notMerge push\n      // without start/end would reset the zoom to the full extent.\n      start: brushRange.start,\n      end: brushRange.end,\n      brushSelect: false,\n      // Range labels are overlay pills below the frame (see\n      // syncBrushOverlay) — the native detail text renders INSIDE the\n      // track, which is not the evil-brush look.\n      showDetail: false,\n      backgroundColor: \"transparent\",\n      // The visible frame is the graphic overlay riding the selection —\n      // the component's own static border stays hidden.\n      borderColor: \"transparent\",\n      fillerColor,\n      dataBackground: { lineStyle: { opacity: 0 }, areaStyle: { opacity: 0 } },\n      selectedDataBackground: { lineStyle: { opacity: 0 }, areaStyle: { opacity: 0 } },\n      // Interaction only — the visible pills are graphic overlays (see\n      // syncBrushOverlay). Kept generous for an easy grab target.\n      handleIcon: \"path://M -3 -5 L -3 5 A 3 3 0 0 0 3 5 L 3 -5 A 3 3 0 0 0 -3 -5 Z\",\n      handleSize: \"35%\",\n      handleStyle: { opacity: 0 },\n      moveHandleSize: 0,\n      emphasis: { handleStyle: { opacity: 0 } },\n    },\n    { type: \"inside\", xAxisIndex: [0] },\n  ];\n}\n",
      "type": "registry:component",
      "target": "components/evilcharts/ui/echarts-brush.tsx"
    }
  ],
  "type": "registry:component"
}