{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gpuix-data",
  "type": "registry:lib",
  "title": "Runtime: data",
  "description": "The `@gpuixui/data` modules the component items import. Installed automatically with the component; not a component itself.",
  "author": "Cyberistic <https://github.com/Cyberistic>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "@tanstack/charts@1.2.1",
    "@tanstack/pacer@0.23.1",
    "@tanstack/table-core@9.3.1",
    "@tanstack/virtual-core@3.19.6",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-components.json",
    "https://gpuixui.sudokw.com/gpuix-icons.json",
    "https://gpuixui.sudokw.com/gpuix-motion.json",
    "https://gpuixui.sudokw.com/gpuix-primitives.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/data/src/chart/animation.ts",
      "target": "@ui/gpuixui/data/chart/animation.ts",
      "type": "registry:lib",
      "content": "import { useMotionPreference } from \"../../motion/react.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { ChartSeries } from \"./logic.js\";\n\nexport const ANIMATION_FRAME_MS = 16;\n\nexport const DEFAULT_ANIMATION_DURATION = 1500;\n\nexport const easeOutCubic = (progress: number): number => {\n  const t = Math.max(0, Math.min(1, Number.isFinite(progress) ? progress : 0));\n  return 1 - (1 - t) ** 3;\n};\n\nexport const between = (from: number, to: number, progress: number): number => {\n  const t = Math.max(0, Math.min(1, Number.isFinite(progress) ? progress : 0));\n  const start = Number.isFinite(from) ? from : 0;\n  const end = Number.isFinite(to) ? to : 0;\n  return start + (end - start) * t;\n};\n\nexport const interpolateSeries = (\n  from: ReadonlyArray<ChartSeries>,\n  to: ReadonlyArray<ChartSeries>,\n  progress: number,\n): ReadonlyArray<ChartSeries> => {\n  const t = Math.max(0, Math.min(1, Number.isFinite(progress) ? progress : 0));\n  return Arr.map(to, (target) => {\n    const before = Option.match(\n      Arr.findFirst(from, (one) => one.key === target.key),\n      {\n        onNone: () => [] as ReadonlyArray<number>,\n        onSome: (one) => one.values,\n      },\n    );\n    return {\n      ...target,\n      values: Arr.map(target.values, (value, index) =>\n        between(before[index] ?? 0, value, t),\n      ),\n    };\n  });\n};\n\nexport const interpolateValues = (\n  from: ReadonlyArray<number>,\n  to: ReadonlyArray<number>,\n  progress: number,\n): ReadonlyArray<number> =>\n  Arr.map(to, (value, index) => between(from[index] ?? 0, value, progress));\n\nexport const scaleValue = (value: number, progress: number): number =>\n  !Number.isFinite(value) || progress >= 1\n    ? value\n    : value === 0\n      ? 0\n      : between(0, value, progress);\n\nexport const scaleValues = (\n  values: ReadonlyArray<number>,\n  progress: number,\n): ReadonlyArray<number> =>\n  Arr.map(values, (value) => scaleValue(value, progress));\n\nexport const scaleSeries = (\n  series: ReadonlyArray<ChartSeries>,\n  progress: number,\n): ReadonlyArray<ChartSeries> =>\n  Arr.map(series, (one) => ({\n    ...one,\n    values: scaleValues(one.values, progress),\n  }));\n\nexport const signatureOf = (\n  values: ReadonlyArray<ReadonlyArray<number>>,\n): string =>\n  Arr.join(\n    Arr.map(values, (one) => Arr.join(Arr.map(one, String), \",\")),\n    \"|\",\n  );\n\nconst now = (): number => Date.now();\n\nconst useTween = <A>(\n  value: A,\n  signature: string,\n  blend: (from: A, to: A, progress: number) => A,\n  animate: boolean,\n  duration: number,\n  reduced: boolean,\n): A => {\n  const durationMs = Number.isFinite(duration) ? Math.max(0, duration) : 0;\n  const running = animate && !reduced && durationMs > 0;\n  const [painted, setPainted] = useState(value);\n  const from = useRef(value);\n  useEffect(() => {\n    if (!running) {\n      from.current = value;\n      setPainted(value);\n      return;\n    }\n    const start = now();\n    let handle: ReturnType<typeof setTimeout> | undefined;\n    const tick = (): void => {\n      const t = Math.min(1, (now() - start) / durationMs);\n      setPainted(blend(from.current, value, easeOutCubic(t)));\n      if (t < 1) {\n        handle = setTimeout(tick, ANIMATION_FRAME_MS);\n      } else {\n        from.current = value;\n      }\n    };\n    handle = setTimeout(tick, 0);\n    return () => {\n      if (handle !== undefined) clearTimeout(handle);\n    };\n  }, [signature, running, durationMs]);\n  return running ? painted : value;\n};\n\nexport const useMountReveal = (animate: boolean, duration: number): number => {\n  const reduced = useMotionPreference() === \"reduced\";\n  const durationMs = Number.isFinite(duration) ? Math.max(0, duration) : 0;\n  const running = animate && !reduced && durationMs > 0;\n  const [progress, setProgress] = useState(running ? 0 : 1);\n  useEffect(() => {\n    if (!running) {\n      setProgress(1);\n      return;\n    }\n    const start = now();\n    let handle: ReturnType<typeof setTimeout> | undefined;\n    const tick = (): void => {\n      const t = Math.min(1, (now() - start) / durationMs);\n      setProgress(easeOutCubic(t));\n      if (t < 1) {\n        handle = setTimeout(tick, ANIMATION_FRAME_MS);\n      }\n    };\n    handle = setTimeout(tick, 0);\n    return () => {\n      if (handle !== undefined) clearTimeout(handle);\n    };\n  }, [running, durationMs]);\n  return running ? progress : 1;\n};\n\n/** The values a chart paints, tweened from the set it last showed. */\nexport const useAnimatedValues = (\n  values: ReadonlyArray<ReadonlyArray<number>>,\n  animate: boolean,\n  duration: number,\n): ReadonlyArray<ReadonlyArray<number>> => {\n  const reduced = useMotionPreference() === \"reduced\";\n  return useTween(\n    values,\n    signatureOf(values),\n    interpolateValues2,\n    animate,\n    duration,\n    reduced,\n  );\n};\n\n/** Two lists blended, for the values hook. */\nconst interpolateValues2 = (\n  from: ReadonlyArray<ReadonlyArray<number>>,\n  to: ReadonlyArray<ReadonlyArray<number>>,\n  progress: number,\n): ReadonlyArray<ReadonlyArray<number>> =>\n  Arr.map(to, (one, index) =>\n    interpolateValues(from[index] ?? [], one, progress),\n  );\n\n/** The series a chart paints, tweened from the set it last showed. */\nexport const useAnimatedSeries = (\n  series: ReadonlyArray<ChartSeries>,\n  animate: boolean,\n  duration: number,\n): ReadonlyArray<ChartSeries> => {\n  const reduced = useMotionPreference() === \"reduced\";\n  return useTween(\n    series,\n    signatureOf(Arr.map(series, (one) => one.values)),\n    interpolateSeries,\n    animate,\n    duration,\n    reduced,\n  );\n};\n"
    },
    {
      "path": "packages/data/src/chart/chart.tsx",
      "target": "@ui/gpuixui/data/chart/chart.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\n\nimport type { Props } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { Icon, isIconName, type IconName } from \"../../icons/index.js\";\nimport { spokenText } from \"../../primitives/core/index.js\";\nimport { themeModeOf } from \"../../tokens/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as Str from \"effect/String\";\nimport { listOf, recordsOf, textsOf } from \"../internal/list.js\";\nimport type { ChartBox, ChartMark, ChartSeries } from \"./logic.js\";\nimport {\n  categoriesFromData,\n  configSeriesColour,\n  seriesFromConfig,\n  type ChartConfig,\n  type ChartRow,\n} from \"./config.js\";\nimport { lineStroke } from \"./mark.js\";\nimport {\n  chartBox,\n  chartScale,\n  seriesDescription,\n  seriesName,\n} from \"./logic.js\";\nimport {\n  areaBands,\n  areaPath,\n  barMarksInRange,\n  candleAtMarks,\n  candleUp,\n  chartRange,\n  curvePath,\n  emphasisedPointMarks,\n  funnelMarks,\n  horizontalBarMarks,\n  pointMarks,\n  polygonPath,\n  radarCircleGrid,\n  radarGrid,\n  radarPoints,\n  radarSpokes,\n  radialArcs,\n  fullRing,\n  scatterMarks,\n  chartView,\n  chartWindow,\n  radarCircleGridAt,\n  radarGridAt,\n  ringBands,\n  ringLabelPoints,\n  ringOutsideLabels,\n  ringSegments,\n  scaleY,\n  seriesPoints,\n  valueLabel,\n  type Candle,\n  type ChartView,\n  type Curve,\n} from \"./geometry.js\";\nimport {\n  DEFAULT_ANIMATION_DURATION,\n  scaleSeries,\n  scaleValue,\n  useAnimatedValues,\n  useMountReveal,\n} from \"./animation.js\";\nimport type { MarkClip } from \"./mark.js\";\nimport {\n  ChartFrame,\n  ChartMask,\n  PointTooltip,\n  paletteOf,\n  seriesColourOf,\n  type LegendShape,\n  type LegendSide,\n  type TooltipIndicator,\n  type PaintContext,\n  type PaintLayer,\n  type ValueLabel,\n} from \"./frame.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { ChartLegend } from \"./legend.js\";\nimport { textStyle, themeColors, useTheme } from \"../theme.js\";\nimport { CHART_PLOT, CHART_ROOT } from \"./recipe.js\";\n\nexport {\n  CHART_AXIS_WIDTH,\n  DEFAULT_CHART_HEIGHT,\n  DEFAULT_CHART_WIDTH,\n} from \"./frame.js\";\nexport {\n  ChartLegend,\n  type ChartLegendItem,\n  type ChartLegendProps,\n} from \"./legend.js\";\nexport type { LegendShape, TooltipIndicator } from \"./frame.js\";\n\nconst DEFAULT_RING_SIZE = 160;\n\nconst DEFAULT_SPARKLINE_WIDTH = 120;\n\nconst DEFAULT_SPARKLINE_HEIGHT = 32;\n\nexport const DEFAULT_SPARKLINE_LABEL = \"Trend\";\n\nconst chosenColour = (colour: string | undefined, fallback: string): string =>\n  Option.match(Option.fromNullishOr(colour), {\n    onNone: () => fallback,\n    onSome: (value) => (Str.isNonEmpty(Str.trim(value)) ? value : fallback),\n  });\n\nconst revealOf = (reveal: number | undefined): number => reveal ?? 1;\n\nconst revealClip = (\n  reveal: number | undefined,\n  box: ChartBox,\n): MarkClip | undefined =>\n  revealOf(reveal) >= 1\n    ? undefined\n    : { x: 0, y: 0, width: box.width * revealOf(reveal), height: box.height };\n\nconst scaleCandles = (\n  candles: ReadonlyArray<Candle>,\n  progress: number,\n): ReadonlyArray<Candle> =>\n  Arr.map(candles, (candle) => ({\n    open: scaleValue(candle.open, progress),\n    high: scaleValue(candle.high, progress),\n    low: scaleValue(candle.low, progress),\n    close: scaleValue(candle.close, progress),\n  }));\n\nexport interface ChartProps extends Omit<Props, \"children\"> {\n  readonly series?: ReadonlyArray<ChartSeries>;\n  readonly labels?: ReadonlyArray<string>;\n  readonly config?: ChartConfig;\n  readonly data?: ReadonlyArray<ChartRow>;\n  readonly width?: number;\n  readonly height?: number;\n  readonly legend?: boolean | LegendSide;\n  readonly legendShape?: LegendShape;\n  readonly axis?: boolean;\n  readonly grid?: boolean;\n  readonly xLabels?: boolean;\n  readonly ticks?: number;\n  readonly colors?: ReadonlyArray<string>;\n  readonly tooltip?: boolean;\n  readonly tooltipIndicator?: TooltipIndicator;\n  readonly tooltipIcons?: ReadonlyArray<IconName>;\n  readonly labelFormat?: (label: string) => string;\n  readonly activeBand?: boolean;\n  readonly crosshair?: boolean;\n  readonly domain?: readonly [number, number];\n  readonly view?: readonly [number, number];\n  readonly zoom?: boolean;\n  readonly onViewChange?: (view: readonly [number, number]) => void;\n  readonly brush?: boolean;\n  readonly onDomainChange?: (domain: readonly [number, number]) => void;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly valueFormat?: (value: number) => string;\n  readonly className?: string;\n  readonly testId?: string;\n}\n\nconst seriesOf = (\n  series: ReadonlyArray<ChartSeries> | undefined,\n): ReadonlyArray<ChartSeries> =>\n  Arr.map(recordsOf(series), (one) => ({\n    ...one,\n    values: listOf(one.values),\n  }));\n\nconst seriesFor = (\n  series: ReadonlyArray<ChartSeries> | undefined,\n  config: ChartConfig | undefined,\n  data: ReadonlyArray<ChartRow> | undefined,\n): ReadonlyArray<ChartSeries> =>\n  config === undefined || data === undefined\n    ? seriesOf(series)\n    : seriesFromConfig(config, data);\n\nconst labelsFor = (\n  labels: ReadonlyArray<string> | undefined,\n  data: ReadonlyArray<ChartRow> | undefined,\n): ReadonlyArray<string> => {\n  const own = labelsOf(labels);\n  if (data === undefined || data.length === 0) return own;\n  const fromData = categoriesFromData(data);\n  return fromData.length === 0 ? own : fromData;\n};\n\nconst labelsOf = (\n  labels: ReadonlyArray<string> | undefined,\n): ReadonlyArray<string> => textsOf(labels);\n\nconst viewOf = (\n  labels: ReadonlyArray<string>,\n  view: readonly [number, number] | undefined,\n): ChartView | undefined =>\n  Option.getOrUndefined(chartView(labels.length, view));\n\nconst windowOf = (\n  rows: ReadonlyArray<ChartSeries>,\n  labels: ReadonlyArray<string>,\n  domain: readonly [number, number] | undefined,\n): {\n  readonly rows: ReadonlyArray<ChartSeries>;\n  readonly labels: ReadonlyArray<string>;\n} =>\n  Option.match(chartWindow(labels.length, domain), {\n    onNone: () => ({ rows, labels }),\n    onSome: (window) => {\n      const width = window.end - window.start;\n      return {\n        rows: Arr.map(rows, (one) => ({\n          ...one,\n          values: Arr.take(Arr.drop(one.values, window.start), width),\n        })),\n        labels: Arr.take(Arr.drop(labels, window.start), width),\n      };\n    },\n  });\n\nconst dataOf = (\n  data: ReadonlyArray<ChartDatum> | undefined,\n): ReadonlyArray<ChartDatum> =>\n  Arr.map(recordsOf(data), (one) => ({\n    ...one,\n    value: one.value,\n  }));\n\nconst layersFor = (\n  series: ReadonlyArray<ChartSeries>,\n  layers: ReadonlyArray<ReadonlyArray<ChartMark>>,\n): ReadonlyArray<PaintLayer> =>\n  Arr.flatMap(series, (one, seriesIndex) => [\n    {\n      key: one.key,\n      series: one,\n      seriesIndex,\n      marks: layers[seriesIndex] ?? [],\n    },\n  ]);\n\nconst barValueLabels =\n  (\n    format: (value: number, one: ChartSeries, category: string) => string,\n    horizontal: boolean,\n    view: ChartView | undefined,\n  ) =>\n  ({\n    series,\n    box,\n    range,\n    labels,\n    reveal,\n  }: PaintContext): ReadonlyArray<ValueLabel> => {\n    if (revealOf(reveal) < 1) return [];\n    if (series.length !== 1) return [];\n    const [layer] = horizontal\n      ? horizontalBarMarks(series, box, range, labels.length)\n      : barMarksInRange(series, box, range, labels.length, 0, view);\n    const one = series[0];\n    if (one === undefined) return [];\n    return Arr.flatMap(layer ?? [], (mark, index) => {\n      if (mark.kind !== \"rect\") return [];\n      return [\n        {\n          key: one.key,\n          x: horizontal ? mark.x + mark.width + 4 : mark.x,\n          y: horizontal\n            ? mark.y + mark.height / 2 - 8\n            : Math.max(0, mark.y - 16),\n          width: horizontal ? 40 : mark.width,\n          text: format(one.values[index] ?? 0, one, labels[index] ?? one.key),\n        },\n      ];\n    });\n  };\n\nconst pointValueLabels =\n  (format: (value: number) => string) =>\n  ({\n    series,\n    box,\n    range,\n    labels,\n    reveal,\n  }: PaintContext): ReadonlyArray<ValueLabel> => {\n    if (revealOf(reveal) < 1) return [];\n    if (series.length !== 1) return [];\n    const [one] = series;\n    if (one === undefined) return [];\n    const points = seriesPoints(one.values, box, range, labels.length);\n    return Arr.map(points, (point, index) => ({\n      key: one.key,\n      x: point.x - 24,\n      y: Math.max(0, point.y - 16),\n      width: 48,\n      text: format(one.values[index] ?? 0),\n    }));\n  };\n\nexport interface BarChartProps extends ChartProps {\n  readonly orientation?: \"vertical\" | \"horizontal\";\n  readonly radius?: number;\n  readonly valueLabels?: boolean;\n  readonly valueLabelFormat?: (\n    value: number,\n    series: ChartSeries,\n    category: string,\n  ) => string;\n}\n\nexport const BarChart = ({\n  orientation = \"vertical\",\n  radius = 3,\n  valueLabels = false,\n  valueLabelFormat,\n  valueFormat,\n  ...props\n}: BarChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = windowed.labels;\n  const range = chartRange(rows, labels.length);\n  const view = viewOf(labels, props.view);\n  const horizontal = orientation === \"horizontal\";\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      range={range}\n      grid={props.grid ?? !horizontal}\n      paint={({ series, labels, box, reveal }) => {\n        const shown = scaleSeries(series, revealOf(reveal));\n        return layersFor(\n          shown,\n          horizontal\n            ? horizontalBarMarks(shown, box, range, labels.length, radius)\n            : barMarksInRange(shown, box, range, labels.length, radius, view),\n        );\n      }}\n      valueLabels={\n        valueLabels\n          ? barValueLabels(\n              (value, one, category) =>\n                valueLabelFormat === undefined\n                  ? (valueFormat ?? valueLabel)(value)\n                  : valueLabelFormat(value, one, category),\n              horizontal,\n              view,\n            )\n          : undefined\n      }\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport interface StackedBarProps extends ChartProps {\n  readonly radius?: number;\n  readonly percent?: boolean;\n}\n\nconst percentOf = (\n  series: ReadonlyArray<ChartSeries>,\n  categories: number,\n): ReadonlyArray<ChartSeries> =>\n  Arr.map(series, (one) => ({\n    ...one,\n    values: Arr.map(Arr.take(one.values, categories), (value, index) => {\n      const total = Arr.reduce(\n        series,\n        0,\n        (sum, entry) => sum + Math.max(0, entry.values[index] ?? 0),\n      );\n      return total <= 0 ? 0 : (Math.max(0, value) / total) * 100;\n    }),\n  }));\n\nexport const StackedBar = ({\n  radius = 3,\n  percent = false,\n  ...props\n}: StackedBarProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const labels = windowed.labels;\n  const source = windowed.rows;\n  const rows = percent ? percentOf(source, labels.length) : source;\n  const range = percent\n    ? { min: 0, max: 100 }\n    : chartRange(rows, labels.length, true);\n  const view = viewOf(labels, props.view);\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      range={range}\n      paint={({ series, labels: slots, box, reveal }) => {\n        const shown = scaleSeries(series, revealOf(reveal));\n        return layersFor(\n          shown,\n          stackedLayers(shown, box, range, slots.length, radius, view),\n        );\n      }}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nconst stackedLayers = (\n  series: ReadonlyArray<ChartSeries>,\n  box: ChartBox,\n  range: { readonly min: number; readonly max: number },\n  categories: number,\n  radius: number,\n  view?: ChartView,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const span = view === undefined ? categories : view.end - view.start;\n  const offset = view === undefined ? 0 : view.start;\n  const slot = box.width / Math.max(1, span);\n  const width = series.length === 0 ? 0 : Math.max(1, (slot * 0.8) / 1);\n  const [, levels] = Arr.mapAccum(series, [] as Array<number>, (stack, one) => {\n    const next = Arr.makeBy(Math.max(0, categories), (index) => {\n      const sum = (stack[index] ?? 0) + Math.max(0, one.values[index] ?? 0);\n      return Number.isFinite(sum) ? sum : (stack[index] ?? 0);\n    });\n    return [next, next];\n  });\n  return Arr.map(series, (_, seriesIndex) => {\n    const upper = levels[seriesIndex] ?? [];\n    const lower = seriesIndex === 0 ? [] : (levels[seriesIndex - 1] ?? []);\n    return Arr.flatMap(upper, (value, index) => {\n      const top = scaleY(value, box, range);\n      const bottom = scaleY(lower[index] ?? 0, box, range);\n      const height = Math.abs(bottom - top);\n      if (height < 0.5) return [];\n      return [\n        {\n          kind: \"rect\" as const,\n          x: (index - offset) * slot + (slot - width) / 2,\n          y: Math.min(top, bottom),\n          width,\n          height,\n          rx: Math.min(radius, width / 2, height / 2),\n        },\n      ];\n    });\n  });\n};\n\nexport interface ComposedChartProps extends ChartProps {\n  readonly lineKeys?: ReadonlyArray<string>;\n  readonly radius?: number;\n}\n\nexport const ComposedChart = ({\n  lineKeys = [],\n  radius = 3,\n  ...props\n}: ComposedChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = windowed.labels;\n  const range = chartRange(rows, labels.length);\n  const view = viewOf(labels, props.view);\n  const isLine = (key: string): boolean =>\n    Arr.some(lineKeys, (candidate) => candidate === key);\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      view={props.view}\n      range={range}\n      paint={({ series, labels: slots, box, reveal }) => {\n        const shown = scaleSeries(series, revealOf(reveal));\n        const clip = revealClip(reveal, box);\n        const bars = Arr.filter(shown, (one) => !isLine(one.key));\n        const barMarks = barMarksInRange(\n          bars,\n          box,\n          range,\n          slots.length,\n          radius,\n          view,\n        );\n        return Arr.flatten([\n          Arr.map(bars, (one, barIndex) => ({\n            key: one.key,\n            series: one,\n            seriesIndex: Arr.findFirstIndex(\n              series,\n              (entry) => entry.key === one.key,\n            ).pipe(Option.getOrElse(() => barIndex)),\n            marks: barMarks[barIndex] ?? [],\n          })),\n          Arr.flatMap(shown, (one, seriesIndex) => {\n            if (!isLine(one.key)) return [];\n            const points = seriesPoints(one.values, box, range, slots.length);\n            return [\n              {\n                key: one.key,\n                series: one,\n                seriesIndex,\n                ...(clip === undefined ? {} : { clip }),\n                marks: Option.match(curvePath(points, \"linear\"), {\n                  onNone: () => [],\n                  onSome: (d): ReadonlyArray<ChartMark> => [\n                    { kind: \"path\" as const, d, stroke: lineStroke(2) },\n                  ],\n                }),\n              } satisfies PaintLayer,\n            ];\n          }),\n        ]);\n      }}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport interface LineChartProps extends ChartProps {\n  readonly curve?: Curve;\n  readonly dots?: boolean;\n  readonly dashed?: boolean;\n  readonly lineWidth?: number;\n  readonly showLabels?: boolean;\n  readonly dotIcons?: ReadonlyArray<IconName>;\n  readonly pointTooltip?: boolean;\n}\n\nexport interface ScatterChartProps extends ChartProps {\n  readonly radius?: number;\n}\n\nexport const ScatterChart = ({ radius = 3, ...props }: ScatterChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = windowed.labels;\n  const range = chartRange(rows, labels.length);\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      range={range}\n      paint={({ series, labels: slots, box, reveal }) => {\n        const progress = revealOf(reveal);\n        return progress <= 0\n          ? layersFor(\n              series,\n              Arr.map(series, () => []),\n            )\n          : layersFor(\n              series,\n              Arr.map(series, (one) =>\n                scatterMarks(\n                  one.values,\n                  box,\n                  range,\n                  slots.length,\n                  radius * progress,\n                ),\n              ),\n            );\n      }}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport interface CandleChartProps extends ChartProps {\n  readonly candles: ReadonlyArray<Candle>;\n  readonly width?: number;\n  readonly upColor?: string;\n  readonly downColor?: string;\n}\n\nexport const CandleChart = ({\n  candles,\n  width = 6,\n  upColor,\n  downColor,\n  ...props\n}: CandleChartProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = windowed.labels;\n  const range = chartRange(\n    [\n      {\n        key: \"close\",\n        label: \"Close\",\n        values: Arr.map(candles, (one) => one.close),\n      },\n    ],\n    labels.length,\n  );\n  const up = upColor ?? colors.success ?? \"#22c55e\";\n  const down = downColor ?? colors.destructive ?? \"#ef4444\";\n  const tweened = useAnimatedValues(\n    [\n      Arr.map(candles, (one) => one.open),\n      Arr.map(candles, (one) => one.high),\n      Arr.map(candles, (one) => one.low),\n      Arr.map(candles, (one) => one.close),\n    ],\n    props.animate ?? true,\n    props.animationDuration ?? DEFAULT_ANIMATION_DURATION,\n  );\n  const shownCandles = Arr.map(candles, (candle, index) => ({\n    open: tweened[0]?.[index] ?? candle.open,\n    high: tweened[1]?.[index] ?? candle.high,\n    low: tweened[2]?.[index] ?? candle.low,\n    close: tweened[3]?.[index] ?? candle.close,\n  }));\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      range={range}\n      paint={({ box, reveal }) => [\n        {\n          key: \"candles\",\n          series: { key: \"candles\", label: \"Candles\", values: [] },\n          seriesIndex: 0,\n          marks:\n            revealOf(reveal) <= 0\n              ? []\n              : Arr.flatMap(\n                  scaleCandles(shownCandles, revealOf(reveal)),\n                  (candle, index) =>\n                    Arr.map(\n                      candleAtMarks(\n                        candle,\n                        index,\n                        labels.length,\n                        box,\n                        range,\n                        width,\n                      ),\n                      (mark) => ({\n                        ...mark,\n                        fill: { color: candleUp(candle) ? up : down },\n                      }),\n                    ),\n                ),\n        },\n      ]}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport interface FunnelChartProps extends ChartProps {\n  readonly stages: ReadonlyArray<number>;\n  readonly stageLabels?: ReadonlyArray<string>;\n}\n\nexport const FunnelChart = ({\n  stages,\n  stageLabels,\n  ...props\n}: FunnelChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = stageLabels ?? labelsFor(props.labels, props.data);\n  const range = chartRange(\n    [{ key: \"stage\", label: \"Stage\", values: stages }],\n    labels.length,\n  );\n  const tweened =\n    useAnimatedValues(\n      [stages],\n      props.animate ?? true,\n      props.animationDuration ?? DEFAULT_ANIMATION_DURATION,\n    )[0] ?? stages;\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      range={range}\n      paint={({ box, reveal }) => {\n        const shown = Arr.map(tweened, (value) =>\n          scaleValue(value, revealOf(reveal)),\n        );\n        return [\n          {\n            key: \"funnel\",\n            series: { key: \"funnel\", label: \"Funnel\", values: shown },\n            seriesIndex: 0,\n            marks: funnelMarks(shown, box),\n          },\n        ];\n      }}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport const LineChart = ({\n  curve = \"linear\",\n  dots = false,\n  dashed = false,\n  lineWidth = 2,\n  showLabels = false,\n  dotIcons,\n  pointTooltip = false,\n  ...props\n}: LineChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = windowed.labels;\n  const range = chartRange(rows, labels.length);\n  const view = viewOf(labels, props.view);\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      view={props.view}\n      range={range}\n      paint={({ series, labels, box, hovered, reveal }) => {\n        const clip = revealClip(reveal, box);\n        return Arr.flatMap(series, (one, seriesIndex) => {\n          const points = seriesPoints(\n            one.values,\n            box,\n            range,\n            labels.length,\n            view,\n          );\n          const path = curvePath(points, curve);\n          const line: PaintLayer = {\n            key: one.key,\n            series: one,\n            seriesIndex,\n            ...(clip === undefined ? {} : { clip }),\n            marks: Option.match(path, {\n              onNone: () => [],\n              onSome: (d): ReadonlyArray<ChartMark> => [\n                { kind: \"path\", d, stroke: lineStroke(lineWidth, dashed) },\n              ],\n            }),\n          };\n          return dots\n            ? [\n                {\n                  ...line,\n                  kind: \"dots\",\n                  decorative: true,\n                  marks: emphasisedPointMarks(\n                    points,\n                    lineWidth + 1.5,\n                    Option.getOrUndefined(hovered ?? Option.none()),\n                  ),\n                },\n                line,\n              ]\n            : [line];\n        });\n      }}\n      valueLabels={\n        showLabels\n          ? pointValueLabels(props.valueFormat ?? valueLabel)\n          : undefined\n      }\n      overlay={\n        dotIcons === undefined && !pointTooltip\n          ? undefined\n          : ({ series, labels, box, range, palette, origin, reveal }) => {\n              const zones = pointTooltip\n                ? Arr.flatMap(series, (one, seriesIndex) =>\n                    Arr.map(\n                      seriesPoints(one.values, box, range, labels.length, view),\n                      (point, index) => ({\n                        key: one.key,\n                        x: point.x,\n                        y: point.y,\n                        category: labels[index] ?? \"\",\n                        label: seriesName(one.label, one.key),\n                        value: (props.valueFormat ?? valueLabel)(\n                          one.values[index] ?? 0,\n                        ),\n                        colour: seriesColourOf(palette, one, seriesIndex),\n                      }),\n                    ),\n                  )\n                : [];\n              return (\n                <>\n                  {pointTooltip ? (\n                    <PointTooltip\n                      zones={zones}\n                      box={box}\n                      origin={origin}\n                      testId={props.testId}\n                    />\n                  ) : null}\n                  {dotIcons === undefined || revealOf(reveal) < 1\n                    ? null\n                    : Arr.flatMap(series, (one, seriesIndex) =>\n                        Arr.map(\n                          seriesPoints(one.values, box, range, labels.length),\n                          (point, index) => {\n                            const name = dotIcons[index];\n                            return name === undefined ||\n                              !isIconName(name) ? null : (\n                              <div\n                                key={`${one.key}-icon-${index}`}\n                                style={{\n                                  position: \"absolute\",\n                                  left: point.x - 7,\n                                  top: point.y - 7,\n                                }}\n                              >\n                                <Icon\n                                  color={seriesColourOf(\n                                    palette,\n                                    one,\n                                    seriesIndex,\n                                  )}\n                                  name={name}\n                                  size={14}\n                                  testId={\n                                    props.testId === undefined\n                                      ? undefined\n                                      : `${props.testId}-dot-${one.key}-${index}`\n                                  }\n                                />\n                              </div>\n                            );\n                          },\n                        ),\n                      )}\n                </>\n              );\n            }\n      }\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport interface AreaChartProps extends ChartProps {\n  readonly curve?: Curve;\n  readonly stacked?: boolean;\n  readonly gradient?: boolean;\n  readonly fillOpacity?: number;\n  readonly dots?: boolean;\n  readonly lineWidth?: number;\n  readonly showLabels?: boolean;\n  readonly pointTooltip?: boolean;\n  readonly dotIcons?: ReadonlyArray<IconName>;\n}\n\nexport const AreaChart = ({\n  curve = \"smooth\",\n  stacked = false,\n  gradient = true,\n  fillOpacity = 0.35,\n  dots = false,\n  lineWidth = 2,\n  showLabels = false,\n  pointTooltip = false,\n  dotIcons,\n  ...props\n}: AreaChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const labels = windowed.labels;\n  const source = windowed.rows;\n  const rows = stacked ? cumulativeSeries(source) : source;\n  const range = chartRange(source, labels.length, stacked);\n  const view = viewOf(labels, props.view);\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      view={props.view}\n      range={range}\n      paint={({ series, labels, box, hovered, reveal }) => {\n        const clip = revealClip(reveal, box);\n        return Arr.flatMap(series, (one, seriesIndex) => {\n          const points = seriesPoints(\n            one.values,\n            box,\n            range,\n            labels.length,\n            view,\n          );\n          const fill = areaPath(points, box, range, curve);\n          const bands = gradient ? areaBands(points, box, range, 3) : [];\n          const line = curvePath(points, curve);\n          return Arr.flatten([\n            [\n              {\n                key: one.key,\n                series: one,\n                seriesIndex,\n                opacity: fillOpacity,\n                ...(clip === undefined ? {} : { clip }),\n                marks: Option.match(fill, {\n                  onNone: () => [],\n                  onSome: (d): ReadonlyArray<ChartMark> => [\n                    { kind: \"path\", d },\n                  ],\n                }),\n              } satisfies PaintLayer,\n            ],\n            Arr.map(bands, (d, bandIndex) => ({\n              key: one.key,\n              series: one,\n              seriesIndex,\n              kind: `fade-${bandIndex}`,\n              decorative: true,\n              opacity: gradient ? fillOpacity / 3 : 0,\n              ...(clip === undefined ? {} : { clip }),\n              marks: [{ kind: \"path\" as const, d }],\n            })),\n            gradient\n              ? []\n              : [\n                  {\n                    key: one.key,\n                    series: one,\n                    seriesIndex,\n                    kind: \"fade\",\n                    decorative: true,\n                    ...(clip === undefined ? {} : { clip }),\n                    marks: [],\n                  } satisfies PaintLayer,\n                ],\n            [\n              {\n                key: one.key,\n                series: one,\n                seriesIndex,\n                kind: \"line\",\n                decorative: true,\n                ...(clip === undefined ? {} : { clip }),\n                marks: Option.match(line, {\n                  onNone: () => [],\n                  onSome: (d): ReadonlyArray<ChartMark> => [\n                    { kind: \"path\", d, stroke: lineStroke(lineWidth) },\n                  ],\n                }),\n              } satisfies PaintLayer,\n            ],\n            dots\n              ? [\n                  {\n                    key: one.key,\n                    series: one,\n                    seriesIndex,\n                    kind: \"dots\",\n                    decorative: true,\n                    ...(clip === undefined ? {} : { clip }),\n                    marks: emphasisedPointMarks(\n                      points,\n                      lineWidth + 1.5,\n                      Option.getOrUndefined(hovered ?? Option.none()),\n                    ),\n                  } satisfies PaintLayer,\n                ]\n              : [],\n          ]);\n        });\n      }}\n      valueLabels={\n        showLabels\n          ? pointValueLabels(props.valueFormat ?? valueLabel)\n          : undefined\n      }\n      overlay={\n        pointTooltip || dotIcons !== undefined\n          ? ({\n              series,\n              labels: slots,\n              box,\n              range: window,\n              palette,\n              origin,\n              reveal,\n            }) => (\n              <>\n                {pointTooltip ? (\n                  <PointTooltip\n                    zones={Arr.flatMap(series, (one, seriesIndex) =>\n                      Arr.map(\n                        seriesPoints(\n                          one.values,\n                          box,\n                          window,\n                          slots.length,\n                          view,\n                        ),\n                        (point, index) => ({\n                          key: one.key,\n                          x: point.x,\n                          y: point.y,\n                          category: slots[index] ?? \"\",\n                          label: seriesName(one.label, one.key),\n                          value: (props.valueFormat ?? valueLabel)(\n                            one.values[index] ?? 0,\n                          ),\n                          colour: seriesColourOf(palette, one, seriesIndex),\n                        }),\n                      ),\n                    )}\n                    box={box}\n                    origin={origin}\n                    testId={props.testId}\n                  />\n                ) : null}\n                {dotIcons === undefined || revealOf(reveal) < 1\n                  ? null\n                  : Arr.flatMap(series, (one, seriesIndex) =>\n                      Arr.map(\n                        seriesPoints(\n                          one.values,\n                          box,\n                          window,\n                          slots.length,\n                          view,\n                        ),\n                        (point, index) => {\n                          const name = dotIcons[index];\n                          return name === undefined ||\n                            !isIconName(name) ? null : (\n                            <div\n                              key={`${one.key}-icon-${index}`}\n                              style={{\n                                position: \"absolute\",\n                                left: point.x - 7,\n                                top: point.y - 7,\n                              }}\n                            >\n                              <Icon\n                                color={seriesColourOf(\n                                  palette,\n                                  one,\n                                  seriesIndex,\n                                )}\n                                name={name}\n                                size={14}\n                                testId={\n                                  props.testId === undefined\n                                    ? undefined\n                                    : `${props.testId}-dot-${one.key}-${index}`\n                                }\n                              />\n                            </div>\n                          );\n                        },\n                      ),\n                    )}\n              </>\n            )\n          : undefined\n      }\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nconst cumulativeSeries = (\n  series: ReadonlyArray<ChartSeries>,\n): ReadonlyArray<ChartSeries> => {\n  const [, running] = Arr.mapAccum(\n    series,\n    [] as ReadonlyArray<number>,\n    (stack, one) => {\n      const next = Arr.map(\n        one.values,\n        (value, index) => (stack[index] ?? 0) + Math.max(0, value),\n      );\n      return [next, next] as const;\n    },\n  );\n  return Arr.map(series, (one, index) => ({\n    ...one,\n    values: running[index] ?? [],\n  }));\n};\n\nconst LegendFor = ({\n  series,\n  colors,\n  config,\n  shape,\n  testId,\n}: {\n  readonly series: ReadonlyArray<ChartSeries>;\n  readonly colors: ReadonlyArray<string> | undefined;\n  readonly config?: ChartConfig;\n  readonly shape?: LegendShape;\n  readonly testId?: string;\n}) => {\n  const theme = useTheme();\n  const palette = paletteOf(theme, colors);\n  const mode = themeModeOf(theme);\n  return (\n    <ChartLegend\n      shape={shape}\n      testId={testId}\n      items={Arr.map(series, (one, index) => ({\n        key: one.key,\n        label: one.label,\n        color: Option.getOrElse(configSeriesColour(config, one.key, mode), () =>\n          seriesColourOf(palette, one, index),\n        ),\n      }))}\n    />\n  );\n};\n\nexport interface ChartDatum {\n  readonly key: string;\n  readonly label: string;\n  readonly value: number;\n}\n\nexport interface PieChartProps extends Omit<Props, \"children\"> {\n  readonly data: ReadonlyArray<ChartDatum>;\n  readonly size?: number;\n  readonly innerRadius?: number;\n  readonly padAngle?: number;\n  readonly legend?: boolean;\n  readonly legendValues?: boolean;\n  readonly colors?: ReadonlyArray<string>;\n  readonly valueFormat?: (value: number) => string;\n  readonly centerLabel?: string;\n  readonly showLabels?: boolean;\n  readonly labelPosition?: \"inside\" | \"outside\";\n  readonly labelFormat?: (datum: ChartDatum, share: number) => string;\n  readonly activeIndex?: number;\n  readonly stacked?: boolean;\n  readonly onSliceActivate?: (datum: ChartDatum, index: number) => void;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly className?: string;\n  readonly testId?: string;\n}\n\nconst Ring = ({\n  data,\n  size,\n  innerRadius,\n  padAngle,\n  colors,\n  centerLabel,\n  showLabels = false,\n  labelPosition = \"inside\",\n  labelFormat,\n  activeIndex,\n  stacked = false,\n  onSliceActivate,\n  testId,\n}: {\n  readonly data: ReadonlyArray<ChartDatum>;\n  readonly size: number;\n  readonly innerRadius: number;\n  readonly padAngle: number;\n  readonly colors: ReadonlyArray<string> | undefined;\n  readonly centerLabel: string | undefined;\n  readonly showLabels?: boolean;\n  readonly labelPosition?: \"inside\" | \"outside\";\n  readonly labelFormat?: (datum: ChartDatum, share: number) => string;\n  readonly activeIndex?: number;\n  readonly stacked?: boolean;\n  readonly onSliceActivate?: (datum: ChartDatum, index: number) => void;\n  readonly testId: string | undefined;\n}) => {\n  const theme = useTheme();\n  const palette = paletteOf(theme, colors);\n  const box = chartBox(size, size, {\n    width: DEFAULT_RING_SIZE,\n    height: DEFAULT_RING_SIZE,\n  });\n  const slices = Arr.filter(data, (one) => one !== null && one !== undefined);\n  const values = Arr.map(slices, (one) => one.value);\n  const total = Arr.reduce(\n    values,\n    0,\n    (sum, value) => sum + Math.max(0, Number.isFinite(value) ? value : 0),\n  );\n  const segments = stacked\n    ? ringBands(values, box, innerRadius, padAngle, activeIndex)\n    : ringSegments(values, box, innerRadius, padAngle, activeIndex);\n  const labelPoints = ringLabelPoints(values, box, innerRadius);\n  const outsideLabels = ringOutsideLabels(values, box);\n  return (\n    <div\n      testId={testId === undefined ? undefined : `${testId}-plot`}\n      style={{\n        ...recipeStyle(CHART_PLOT, theme),\n        width: box.width,\n        height: box.height,\n      }}\n    >\n      {Arr.map(slices, (one, index) => (\n        <ChartMask\n          key={one.key}\n          chartTestId={testId}\n          layer={{\n            key: one.key,\n            series: { key: one.key, label: one.label, values: [one.value] },\n            seriesIndex: index,\n            marks: segments[index] ?? [],\n          }}\n          label={seriesName(one.label, one.key)}\n          description={seriesDescription([one.value])}\n          colour={seriesColourOf(\n            palette,\n            { key: one.key, label: one.label, values: [] },\n            index,\n          )}\n          box={box}\n        />\n      ))}\n      {onSliceActivate === undefined\n        ? undefined\n        : Arr.map(slices, (one, index) => {\n            const point = labelPoints[index];\n            if (point === undefined || Option.isNone(point)) return null;\n            return (\n              <div\n                key={`${one.key}-hit`}\n                testId={\n                  testId === undefined\n                    ? undefined\n                    : `${testId}-slice-${one.key}`\n                }\n                onClick={() => onSliceActivate(one, index)}\n                style={{\n                  position: \"absolute\",\n                  left: point.value.x - 16,\n                  top: point.value.y - 16,\n                  width: 32,\n                  height: 32,\n                  borderRadius: 16,\n                }}\n              />\n            );\n          })}\n      {showLabels && labelPosition === \"outside\" ? (\n        <ChartMask\n          chartTestId={testId}\n          layer={{\n            key: \"leaders\",\n            series: { key: \"leaders\", label: \"leaders\", values: [] },\n            seriesIndex: 0,\n            decorative: true,\n            opacity: 0.8,\n            marks: Arr.flatMap(outsideLabels, (label) =>\n              Option.match(label, {\n                onNone: () => [],\n                onSome: (found): ReadonlyArray<ChartMark> => [\n                  { kind: \"path\", d: found.leader, stroke: lineStroke(1) },\n                ],\n              }),\n            ),\n          }}\n          label=\"\"\n          description=\"\"\n          colour={themeColors(theme).border ?? \"#000000\"}\n          box={box}\n        />\n      ) : undefined}\n      {showLabels && labelPosition === \"outside\"\n        ? Arr.map(slices, (one, index) => {\n            const label = outsideLabels[index];\n            if (label === undefined || Option.isNone(label)) return null;\n            const share = total <= 0 ? 0 : Math.max(0, one.value) / total;\n            const words =\n              labelFormat === undefined\n                ? seriesName(one.label, one.key)\n                : labelFormat(one, share);\n            const anchor = label.value.point;\n            return (\n              <text\n                key={`${one.key}-outside`}\n                style={{\n                  ...textStyle(theme, \"caption\"),\n                  position: \"absolute\",\n                  left: label.value.right ? anchor.x + 4 : anchor.x - 84,\n                  top: anchor.y - 8,\n                  width: 80,\n                  textAlign: label.value.right ? \"left\" : \"right\",\n                  color: themeColors(theme).mutedForeground,\n                }}\n              >\n                {words}\n              </text>\n            );\n          })\n        : undefined}\n      {showLabels && labelPosition === \"inside\"\n        ? Arr.map(slices, (one, index) => {\n            const point = labelPoints[index];\n            if (point === undefined || Option.isNone(point)) return null;\n            const share = total <= 0 ? 0 : Math.max(0, one.value) / total;\n            const words =\n              labelFormat === undefined\n                ? seriesName(one.label, one.key)\n                : labelFormat(one, share);\n            return (\n              <text\n                key={`${one.key}-label`}\n                style={{\n                  ...textStyle(theme, \"caption\"),\n                  position: \"absolute\",\n                  left: point.value.x - 30,\n                  top: point.value.y - 8,\n                  width: 60,\n                  textAlign: \"center\",\n                  color: themeColors(theme).background,\n                }}\n              >\n                {words}\n              </text>\n            );\n          })\n        : undefined}\n      {centerLabel === undefined ||\n      centerLabel.trim().length === 0 ? undefined : (\n        <text\n          style={{\n            ...textStyle(theme, \"label\"),\n            position: \"absolute\",\n            left: 0,\n            top: box.height / 2 - 10,\n            width: box.width,\n            textAlign: \"center\",\n            color: themeColors(theme).foreground,\n          }}\n        >\n          {centerLabel}\n        </text>\n      )}\n    </div>\n  );\n};\n\nconst RingLegend = ({\n  data,\n  colors,\n  values,\n  format,\n  testId,\n}: {\n  readonly data: ReadonlyArray<ChartDatum>;\n  readonly colors: ReadonlyArray<string> | undefined;\n  readonly values: boolean;\n  readonly format: (value: number) => string;\n  readonly testId: string | undefined;\n}) => {\n  const theme = useTheme();\n  const palette = paletteOf(theme, colors);\n  return (\n    <ChartLegend\n      testId={testId}\n      items={Arr.map(data, (one, index) => ({\n        key: one.key,\n        label: values\n          ? `${seriesName(one.label, one.key)} · ${format(one.value)}`\n          : one.label,\n        color: seriesColourOf(\n          palette,\n          { key: one.key, label: one.label, values: [] },\n          index,\n        ),\n      }))}\n    />\n  );\n};\n\nexport const PieChart = ({\n  data,\n  size = DEFAULT_RING_SIZE,\n  innerRadius = 0,\n  padAngle = 0.02,\n  legend = true,\n  legendValues = false,\n  colors,\n  valueFormat,\n  centerLabel,\n  showLabels = false,\n  labelPosition = \"inside\",\n  labelFormat,\n  activeIndex,\n  stacked = false,\n  onSliceActivate,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  className,\n  testId,\n  style,\n  ...rest\n}: PieChartProps) => {\n  const theme = useTheme();\n  const rows = dataOf(data);\n  const animated = useAnimatedValues(\n    Arr.map(rows, (one) => [one.value]),\n    animate,\n    animationDuration,\n  );\n  const reveal = useMountReveal(animate, animationDuration);\n  const shown = Arr.map(rows, (one, index) => ({\n    ...one,\n    value: scaleValue(animated[index]?.[0] ?? one.value, reveal),\n  }));\n  return (\n    <div\n      {...rest}\n      role=\"group\"\n      testId={testId}\n      style={mergeStyles(recipeStyle(cn(CHART_ROOT, className), theme), style)}\n    >\n      <Ring\n        data={shown}\n        size={size}\n        innerRadius={innerRadius}\n        padAngle={padAngle}\n        colors={colors}\n        centerLabel={centerLabel}\n        showLabels={showLabels && reveal >= 1}\n        labelPosition={labelPosition}\n        labelFormat={labelFormat}\n        activeIndex={activeIndex}\n        stacked={stacked}\n        onSliceActivate={onSliceActivate}\n        testId={testId}\n      />\n      {legend ? (\n        <RingLegend\n          data={rows}\n          colors={colors}\n          values={legendValues}\n          format={valueFormat ?? valueLabel}\n          testId={testId === undefined ? undefined : `${testId}-legend`}\n        />\n      ) : undefined}\n    </div>\n  );\n};\n\nexport type DonutChartProps = PieChartProps;\n\nexport const DonutChart = ({ innerRadius = 0.62, ...props }: PieChartProps) => (\n  <PieChart {...props} innerRadius={innerRadius} />\n);\n\nconst circleAxes = (shape: \"polygon\" | \"circle\", axes: number): number =>\n  shape === \"circle\" ? 4 : axes;\n\nexport interface RadarChartProps extends ChartProps {\n  readonly levels?: number;\n  readonly dots?: boolean;\n  readonly fillOpacity?: number;\n  readonly webs?: boolean;\n  readonly gridShape?: \"polygon\" | \"circle\";\n  readonly gridFill?: boolean;\n  readonly fill?: boolean;\n  readonly radius?: number;\n  readonly gridAt?: ReadonlyArray<number>;\n  readonly dotIcons?: ReadonlyArray<IconName>;\n  readonly axisLabelFormat?: (label: string, index: number) => string;\n  readonly pointTooltip?: boolean;\n}\n\nexport const RadarChart = ({\n  levels = 4,\n  dots = false,\n  fillOpacity = 0.25,\n  webs = true,\n  gridShape = \"polygon\",\n  gridFill = false,\n  fill = true,\n  radius,\n  gridAt,\n  dotIcons,\n  axisLabelFormat,\n  pointTooltip = false,\n  ...props\n}: RadarChartProps) => {\n  const rows = seriesFor(props.series, props.config, props.data);\n  const labels = labelsFor(props.labels, props.data);\n  const axes = Math.max(3, labels.length);\n  const max = chartScale(rows, axes);\n  const range = { min: 0, max };\n  const theme = useTheme();\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      axis={false}\n      grid={false}\n      xLabels={false}\n      range={range}\n      paint={({ series, box, reveal }) => {\n        const shown = scaleSeries(series, revealOf(reveal));\n        return Arr.flatMap(shown, (one, seriesIndex) => {\n          const points = radarPoints(one.values, axes, box, max, radius);\n          const outline = polygonPath(points);\n          return Arr.flatten([\n            webs\n              ? [\n                  {\n                    key: one.key,\n                    series: one,\n                    seriesIndex,\n                    kind: \"web\",\n                    decorative: true,\n                    opacity: 0.6,\n                    marks: Arr.map(\n                      [\n                        ...(gridAt === undefined\n                          ? gridShape === \"circle\"\n                            ? radarCircleGrid(levels, box, radius)\n                            : radarGrid(levels, axes, box, max, radius)\n                          : gridShape === \"circle\"\n                            ? radarCircleGridAt(gridAt, box, radius)\n                            : radarGridAt(gridAt, axes, box, max, radius)),\n                        ...radarSpokes(\n                          circleAxes(gridShape, axes),\n                          box,\n                          max,\n                          radius,\n                        ),\n                      ],\n                      (d): ChartMark => ({\n                        kind: \"path\",\n                        d,\n                        stroke: lineStroke(1),\n                      }),\n                    ),\n                  } satisfies PaintLayer,\n                  ...(gridFill\n                    ? [\n                        {\n                          key: one.key,\n                          series: one,\n                          seriesIndex,\n                          kind: \"web-fill\",\n                          decorative: true,\n                          opacity: 0.06,\n                          marks: Arr.map(\n                            radarGrid(levels, axes, box, max, radius),\n                            (d): ChartMark => ({ kind: \"path\", d }),\n                          ),\n                        } satisfies PaintLayer,\n                      ]\n                    : []),\n                ]\n              : [],\n            [\n              ...(fill\n                ? [\n                    {\n                      key: one.key,\n                      series: one,\n                      seriesIndex,\n                      opacity: fillOpacity,\n                      marks: Option.match(outline, {\n                        onNone: () => [],\n                        onSome: (d): ReadonlyArray<ChartMark> => [\n                          { kind: \"path\" as const, d },\n                        ],\n                      }),\n                    } satisfies PaintLayer,\n                  ]\n                : []),\n              {\n                key: one.key,\n                series: one,\n                seriesIndex,\n                kind: \"line\",\n                decorative: true,\n                marks: Option.match(outline, {\n                  onNone: () => [],\n                  onSome: (d): ReadonlyArray<ChartMark> => [\n                    { kind: \"path\", d, stroke: lineStroke(2) },\n                  ],\n                }),\n              } satisfies PaintLayer,\n            ],\n            dots\n              ? [\n                  {\n                    key: one.key,\n                    series: one,\n                    seriesIndex,\n                    kind: \"dots\",\n                    decorative: true,\n                    marks: pointMarks(points, 3),\n                  } satisfies PaintLayer,\n                ]\n              : [],\n          ]);\n        });\n      }}\n      overlay={({ box, series, palette, origin, reveal }) => (\n        <>\n          {Arr.map(labels, (label, index) => {\n            const angle = -Math.PI / 2 + (index / axes) * Math.PI * 2;\n            const reach =\n              radius === undefined\n                ? Math.min(box.width, box.height) / 2 - 4\n                : radius;\n            const x = box.width / 2 + reach * Math.cos(angle);\n            const y = box.height / 2 + reach * Math.sin(angle);\n            return (\n              <text\n                key={`axis-${index}`}\n                style={{\n                  ...textStyle(theme, \"caption\"),\n                  position: \"absolute\",\n                  left: x - 40,\n                  top: y - 8,\n                  width: 80,\n                  textAlign: \"center\",\n                  color: themeColors(theme).mutedForeground,\n                }}\n              >\n                {axisLabelFormat === undefined\n                  ? label\n                  : axisLabelFormat(label, index)}\n              </text>\n            );\n          })}\n          {pointTooltip && revealOf(reveal) >= 1 ? (\n            <PointTooltip\n              zones={Arr.flatMap(series, (one, seriesIndex) =>\n                Arr.map(\n                  radarPoints(one.values, axes, box, max, radius),\n                  (point, index) => ({\n                    key: one.key,\n                    x: point.x,\n                    y: point.y,\n                    category: labels[index] ?? \"\",\n                    label: seriesName(one.label, one.key),\n                    value: (props.valueFormat ?? valueLabel)(\n                      one.values[index] ?? 0,\n                    ),\n                    colour: seriesColourOf(palette, one, seriesIndex),\n                  }),\n                ),\n              )}\n              box={box}\n              origin={origin}\n              testId={props.testId}\n            />\n          ) : undefined}\n          {dotIcons === undefined || revealOf(reveal) < 1\n            ? undefined\n            : Arr.flatMap(series, (one, seriesIndex) =>\n                Arr.map(\n                  radarPoints(one.values, axes, box, max, radius),\n                  (point, index) => {\n                    const name = dotIcons[index];\n                    return name === undefined || !isIconName(name) ? null : (\n                      <div\n                        key={`${one.key}-icon-${index}`}\n                        style={{\n                          position: \"absolute\",\n                          left: point.x - 7,\n                          top: point.y - 7,\n                        }}\n                      >\n                        <Icon\n                          color={seriesColourOf(palette, one, seriesIndex)}\n                          name={name}\n                          size={14}\n                          testId={\n                            props.testId === undefined\n                              ? undefined\n                              : `${props.testId}-dot-${one.key}-${index}`\n                          }\n                        />\n                      </div>\n                    );\n                  },\n                ),\n              )}\n        </>\n      )}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport interface RadialChartProps extends Omit<Props, \"children\"> {\n  readonly data: ReadonlyArray<ChartDatum>;\n  readonly size?: number;\n  readonly max?: number;\n  readonly thickness?: number;\n  readonly gap?: number;\n  readonly track?: boolean;\n  readonly grid?: boolean;\n  readonly rounded?: boolean;\n  readonly showLabels?: boolean;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly legend?: boolean;\n  readonly colors?: ReadonlyArray<string>;\n  readonly valueFormat?: (value: number) => string;\n  readonly centerLabel?: string;\n  readonly className?: string;\n  readonly testId?: string;\n}\n\nexport const RadialChart = ({\n  data,\n  size = DEFAULT_RING_SIZE,\n  max,\n  thickness = 10,\n  gap = 4,\n  track = false,\n  grid = false,\n  rounded = false,\n  showLabels = false,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  legend = true,\n  colors,\n  valueFormat,\n  centerLabel,\n  className,\n  testId,\n  style,\n  ...rest\n}: RadialChartProps) => {\n  const theme = useTheme();\n  const palette = paletteOf(theme, colors);\n  const rows = dataOf(data);\n  const animated = useAnimatedValues(\n    Arr.map(rows, (one) => [one.value]),\n    animate,\n    animationDuration,\n  );\n  const reveal = useMountReveal(animate, animationDuration);\n  const shown = Arr.map(rows, (one, index) => ({\n    ...one,\n    value: scaleValue(animated[index]?.[0] ?? one.value, reveal),\n  }));\n  const box = chartBox(size, size, {\n    width: DEFAULT_RING_SIZE,\n    height: DEFAULT_RING_SIZE,\n  });\n  const ceiling = Option.match(Option.fromNullishOr(max), {\n    onNone: () =>\n      Arr.reduce(rows, 0, (most, one) =>\n        Math.max(most, Math.max(0, one.value)),\n      ) || 1,\n    onSome: (value) => (Number.isFinite(value) && value > 0 ? value : 1),\n  });\n  const arcs = Arr.map(\n    radialArcs(\n      Arr.map(shown, (one) => one.value),\n      box,\n      ceiling,\n      thickness,\n      gap,\n    ),\n    (marks, index) => {\n      if (!rounded) return marks;\n      const value = shown[index]?.value ?? 0;\n      const span =\n        ceiling <= 0 ? 0 : (Math.max(0, value) / ceiling) * Math.PI * 2;\n      if (span <= 0 || span >= Math.PI * 2 - 1e-9) return marks;\n      const outer =\n        Math.min(box.width, box.height) / 2 - 1 - index * (thickness + gap);\n      const mid = outer - thickness / 2;\n      const cap = (angle: number): ChartMark => ({\n        kind: \"circle\",\n        cx: box.width / 2 + mid * Math.cos(angle),\n        cy: box.height / 2 + mid * Math.sin(angle),\n        r: thickness / 2,\n      });\n      return [...marks, cap(-Math.PI / 2), cap(-Math.PI / 2 + span)];\n    },\n  );\n  return (\n    <div\n      {...rest}\n      role=\"group\"\n      testId={testId}\n      style={mergeStyles(recipeStyle(cn(CHART_ROOT, className), theme), style)}\n    >\n      <div\n        testId={testId === undefined ? undefined : `${testId}-plot`}\n        style={{\n          ...recipeStyle(CHART_PLOT, theme),\n          width: box.width,\n          height: box.height,\n        }}\n      >\n        {grid ? (\n          <ChartMask\n            chartTestId={testId}\n            layer={{\n              key: \"grid\",\n              series: { key: \"grid\", label: \"grid\", values: [] },\n              seriesIndex: 0,\n              decorative: true,\n              opacity: 0.6,\n              marks: Arr.makeBy(4, (index) => ({\n                kind: \"path\" as const,\n                d: fullRing(\n                  box.width / 2,\n                  box.height / 2,\n                  ((Math.min(box.width, box.height) / 2 - 1) * (index + 1)) / 4,\n                  ((Math.min(box.width, box.height) / 2 - 1) * (index + 1)) /\n                    4 -\n                    1,\n                ),\n                stroke: lineStroke(1),\n              })),\n            }}\n            label=\"\"\n            description=\"\"\n            colour={themeColors(theme).border ?? \"#000000\"}\n            box={box}\n          />\n        ) : undefined}\n        {track\n          ? Arr.map(rows, (one, index) => (\n              <ChartMask\n                key={`${one.key}-track`}\n                chartTestId={testId}\n                layer={{\n                  key: one.key,\n                  series: { key: one.key, label: one.label, values: [] },\n                  seriesIndex: index,\n                  kind: \"track\",\n                  decorative: true,\n                  opacity: 0.18,\n                  marks: Arr.makeBy(1, () => {\n                    const outer =\n                      Math.min(box.width, box.height) / 2 -\n                      1 -\n                      index * (thickness + gap);\n                    return {\n                      kind: \"path\" as const,\n                      d: fullRing(\n                        box.width / 2,\n                        box.height / 2,\n                        Math.max(0, outer),\n                        Math.max(0, outer - thickness),\n                      ),\n                    };\n                  }),\n                }}\n                label=\"\"\n                description=\"\"\n                colour={themeColors(theme).muted ?? \"#000000\"}\n                box={box}\n              />\n            ))\n          : undefined}\n        {Arr.map(rows, (one, index) => (\n          <ChartMask\n            key={one.key}\n            chartTestId={testId}\n            layer={{\n              key: one.key,\n              series: { key: one.key, label: one.label, values: [one.value] },\n              seriesIndex: index,\n              marks: arcs[index] ?? [],\n            }}\n            label={seriesName(one.label, one.key)}\n            description={seriesDescription([one.value])}\n            colour={seriesColourOf(\n              palette,\n              { key: one.key, label: one.label, values: [] },\n              index,\n            )}\n            box={box}\n          />\n        ))}\n        {showLabels && reveal >= 1\n          ? Arr.map(rows, (one, index) => {\n              const span =\n                ceiling <= 0\n                  ? 0\n                  : (Math.max(0, one.value) / ceiling) * Math.PI * 2;\n              if (span <= 0) return null;\n              const outer =\n                Math.min(box.width, box.height) / 2 -\n                1 -\n                index * (thickness + gap);\n              const mid = outer - thickness / 2;\n              const angle = -Math.PI / 2 + span;\n              return (\n                <text\n                  key={`${one.key}-value`}\n                  testId={\n                    testId === undefined\n                      ? undefined\n                      : `${testId}-value-${one.key}`\n                  }\n                  style={{\n                    ...textStyle(theme, \"caption\"),\n                    position: \"absolute\",\n                    left: box.width / 2 + mid * Math.cos(angle) + 6,\n                    top: box.height / 2 + mid * Math.sin(angle) - 8,\n                    width: 48,\n                    color: themeColors(theme).mutedForeground,\n                  }}\n                >\n                  {(valueFormat ?? valueLabel)(one.value)}\n                </text>\n              );\n            })\n          : undefined}\n        {centerLabel === undefined ||\n        centerLabel.trim().length === 0 ? undefined : (\n          <text\n            style={{\n              ...textStyle(theme, \"label\"),\n              position: \"absolute\",\n              left: 0,\n              top: box.height / 2 - 10,\n              width: box.width,\n              textAlign: \"center\",\n              color: themeColors(theme).foreground,\n            }}\n          >\n            {centerLabel}\n          </text>\n        )}\n      </div>\n      {legend ? (\n        <RingLegend\n          data={rows}\n          colors={colors}\n          values\n          format={valueFormat ?? valueLabel}\n          testId={testId === undefined ? undefined : `${testId}-legend`}\n        />\n      ) : undefined}\n    </div>\n  );\n};\n\nexport interface SparklineProps extends Omit<Props, \"children\"> {\n  readonly values: ReadonlyArray<number>;\n  readonly width?: number;\n  readonly height?: number;\n  readonly color?: string;\n  readonly curve?: Curve;\n  /** The line is filled down to the floor. Defaults to `false`. */\n  readonly filled?: boolean;\n  /** A data change tweens the line's values. Defaults to `false`. */\n  readonly animate?: boolean;\n  /** How long a data change takes, in milliseconds. Defaults to `320`. */\n  readonly animationDuration?: number;\n  readonly className?: string;\n  readonly testId?: string;\n}\n\nexport const Sparkline = ({\n  values,\n  width = DEFAULT_SPARKLINE_WIDTH,\n  height = DEFAULT_SPARKLINE_HEIGHT,\n  color,\n  curve = \"smooth\",\n  filled = false,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  className,\n  testId,\n  style,\n  \"aria-label\": ariaLabel,\n  ...rest\n}: SparklineProps) => {\n  const theme = useTheme();\n  const box = chartBox(width, height, {\n    width: DEFAULT_SPARKLINE_WIDTH,\n    height: DEFAULT_SPARKLINE_HEIGHT,\n  });\n  const points = Arr.filter(listOf(values), Number.isFinite);\n  const tweened = useAnimatedValues([points], animate, animationDuration);\n  const shownPoints = tweened[0] ?? points;\n  const reveal = useMountReveal(animate, animationDuration);\n  const clip =\n    reveal >= 1\n      ? undefined\n      : { x: 0, y: 0, width: box.width * reveal, height: box.height };\n  const series: ChartSeries = {\n    key: \"sparkline\",\n    label: \"sparkline\",\n    values: shownPoints,\n  };\n  const range = chartRange([series]);\n  const path = curvePath(\n    seriesPoints(shownPoints, box, range, shownPoints.length),\n    curve,\n  );\n  const area = filled\n    ? areaPath(\n        seriesPoints(shownPoints, box, range, shownPoints.length),\n        box,\n        range,\n        curve,\n      )\n    : Option.none();\n  return (\n    <div\n      {...rest}\n      testId={testId}\n      style={mergeStyles(recipeStyle(cn(CHART_ROOT, className), theme), style)}\n    >\n      <div\n        style={{\n          ...recipeStyle(CHART_PLOT, theme),\n          width: box.width,\n          height: box.height,\n        }}\n      >\n        {Option.isNone(area) ? undefined : (\n          <ChartMask\n            chartTestId={testId}\n            layer={{\n              key: \"sparkline\",\n              series,\n              seriesIndex: 0,\n              kind: \"area\",\n              decorative: true,\n              opacity: 0.2,\n              ...(clip === undefined ? {} : { clip }),\n              marks: [{ kind: \"path\", d: Option.getOrElse(area, () => \"\") }],\n            }}\n            label=\"\"\n            description=\"\"\n            colour={chosenColour(\n              color,\n              paletteOf(theme, undefined)[0] ?? \"#000000\",\n            )}\n            box={box}\n          />\n        )}\n        <ChartMask\n          chartTestId={testId}\n          layer={{\n            key: \"sparkline\",\n            series,\n            seriesIndex: 0,\n            ...(clip === undefined ? {} : { clip }),\n            marks: Option.match(path, {\n              onNone: () => [],\n              onSome: (d): ReadonlyArray<ChartMark> => [\n                { kind: \"path\", d, stroke: lineStroke(1.5) },\n              ],\n            }),\n          }}\n          label={spokenText(\n            Option.fromNullishOr(ariaLabel),\n            DEFAULT_SPARKLINE_LABEL,\n          )}\n          description={seriesDescription(shownPoints)}\n          colour={chosenColour(\n            color,\n            paletteOf(theme, undefined)[0] ?? \"#000000\",\n          )}\n          box={box}\n        />\n      </div>\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/chart/config.ts",
      "target": "@ui/gpuixui/data/chart/config.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport type { IconName } from \"../../icons/index.js\";\n\nexport type ChartMode = \"light\" | \"dark\";\n\nexport interface ChartConfigEntry {\n  readonly label?: string;\n\n  readonly icon?: IconName;\n\n  readonly color?: string;\n\n  readonly theme?: Partial<Record<ChartMode, string>>;\n}\n\nexport type ChartConfig = Record<string, ChartConfigEntry | undefined>;\n\nexport const configColour = (\n  entry: ChartConfigEntry | undefined,\n  mode: ChartMode,\n): Option.Option<string> => {\n  if (entry === undefined) return Option.none();\n  const themed = Option.fromNullishOr(entry.theme?.[mode]);\n  const chosen = Option.match(themed, {\n    onNone: () => Option.fromNullishOr(entry.color),\n    onSome: (colour) => Option.some(colour),\n  });\n  return Option.filter(chosen, (colour) => colour.trim().length > 0);\n};\n\nexport const configLabel = (\n  key: string,\n  entry: ChartConfigEntry | undefined,\n): string => {\n  const label = entry?.label;\n  return Option.match(Option.fromNullishOr(label), {\n    onNone: () => key,\n    onSome: (value) => (value.trim().length === 0 ? key : value),\n  });\n};\n\nexport const configIcon = (\n  entry: ChartConfigEntry | undefined,\n): Option.Option<IconName> => Option.fromNullishOr(entry?.icon);\n\nexport const configEntries = (\n  config: ChartConfig | undefined,\n): ReadonlyArray<readonly [string, ChartConfigEntry]> =>\n  Option.match(Option.fromNullishOr(config), {\n    onNone: () => [],\n    onSome: (table) =>\n      Arr.map(Object.keys(table), (key) => {\n        const entry = table[key];\n\n        return [key, entry ?? {}] as const;\n      }),\n  });\n\nexport const configKeys = (\n  config: ChartConfig | undefined,\n): ReadonlyArray<string> => Arr.map(configEntries(config), ([key]) => key);\n\nexport type ChartRow = Record<string, unknown>;\n\nexport const datumNumber = (\n  datum: ChartRow | undefined,\n  field: string,\n): Option.Option<number> => {\n  if (datum === undefined) return Option.none();\n  const value = datum[field];\n  return typeof value === \"number\" && Number.isFinite(value)\n    ? Option.some(value)\n    : Option.none<number>();\n};\n\nexport const seriesFromConfig = (\n  config: ChartConfig | undefined,\n  data: ReadonlyArray<ChartRow>,\n): ReadonlyArray<{\n  readonly key: string;\n  readonly label: string;\n  readonly values: ReadonlyArray<number>;\n}> =>\n  Arr.map(configEntries(config), ([key, entry]) => ({\n    key,\n    label: configLabel(key, entry),\n    values: Arr.map(data, (datum) =>\n      Option.getOrElse(datumNumber(datum, key), () => 0),\n    ),\n  }));\n\nexport const configSeriesColour = (\n  config: ChartConfig | undefined,\n  key: string,\n  mode: ChartMode,\n): Option.Option<string> =>\n  Option.flatMap(Option.fromNullishOr(config), (table) =>\n    configColour(table[key], mode),\n  );\n\nexport const categoriesFromData = (\n  data: ReadonlyArray<ChartRow>,\n): ReadonlyArray<string> => {\n  const first = data[0];\n  if (first === undefined) return [];\n  const field = Arr.findFirst(Object.keys(first), (key) =>\n    Arr.some(data, (datum) => typeof datum[key] === \"string\"),\n  );\n  return Option.match(field, {\n    onNone: () => [],\n    onSome: (name) =>\n      Arr.map(data, (datum) => {\n        const value = datum[name];\n        return typeof value === \"string\" ? value : \"\";\n      }),\n  });\n};\n\nexport const configColours = (\n  config: ChartConfig | undefined,\n  mode: ChartMode,\n): ReadonlyArray<string> =>\n  Arr.map(\n    Arr.filter(configEntries(config), ([, entry]) =>\n      Option.isSome(configColour(entry, mode)),\n    ),\n    ([, entry]) => Option.getOrElse(configColour(entry, mode), () => \"\"),\n  );\n\nexport const configOf = (config: ChartConfig | undefined): ChartConfig =>\n  Option.getOrElse(Option.fromNullishOr(config), () => ({}));\n\nexport const configKeySet = (\n  config: ChartConfig | undefined,\n): ReadonlyArray<string> => Object.keys(configOf(config));\n\nexport const configEntryOf = (\n  config: ChartConfig | undefined,\n  key: string,\n): Option.Option<ChartConfigEntry> =>\n  Option.fromNullishOr(configOf(config)[key]).pipe(\n    Option.map((entry) => entry ?? {}),\n  );\n"
    },
    {
      "path": "packages/data/src/chart/frame.tsx",
      "target": "@ui/gpuixui/data/chart/frame.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\n\nimport type { Props } from \"@gpuix/react\";\nimport { Icon, isIconName, type IconName } from \"../../icons/index.js\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { seriesColors, themeModeOf } from \"../../tokens/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { useRef, useState, type ReactNode } from \"react\";\nimport {\n  chartBox,\n  chartMarkup,\n  seriesDescription,\n  seriesName,\n  tickLabel,\n  type ChartBox,\n  type ChartMark,\n  type ChartSeries,\n} from \"./logic.js\";\nimport type { MarkClip } from \"./mark.js\";\nimport {\n  chartRange,\n  chartView,\n  chartWindow,\n  rangeTicks,\n  valueLabel,\n  type ChartRange,\n  type ChartView,\n  type Point,\n} from \"./geometry.js\";\nimport {\n  DEFAULT_ANIMATION_DURATION,\n  useAnimatedSeries,\n  useMountReveal,\n} from \"./animation.js\";\nimport { ChartPlot } from \"./plot.js\";\nimport type { ChartRender } from \"./surface.js\";\nimport type { ChartGradient } from \"./svg.js\";\nimport { configSeriesColour, type ChartConfig } from \"./config.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { textStyle, themeColors, useTheme } from \"../theme.js\";\nimport {\n  CHART_AXIS,\n  CHART_AXIS_LABEL,\n  CHART_BRUSH,\n  CHART_BRUSH_BAND,\n  CHART_BRUSH_HANDLE,\n  CHART_BRUSH_TRACK,\n  CHART_CATEGORIES,\n  CHART_CATEGORY,\n  CHART_CATEGORY_LABEL,\n  CHART_PLOT,\n  CHART_ROOT,\n  CHART_TOOLTIP,\n  CHART_TOOLTIP_HEAD,\n  CHART_TOOLTIP_ROW,\n  CHART_TOOLTIP_VALUE,\n  CHART_TOOLTIP_ZONE,\n  CHART_VALUE_LABEL,\n} from \"./recipe.js\";\n\nexport const DEFAULT_CHART_WIDTH = 320;\n\nexport const DEFAULT_CHART_HEIGHT = 160;\n\nexport const CHART_AXIS_WIDTH = 36;\n\nexport interface PaintLayer {\n  readonly key: string;\n  readonly series: ChartSeries;\n  readonly seriesIndex: number;\n  readonly marks: ReadonlyArray<ChartMark>;\n  readonly opacity?: number;\n  readonly kind?: string;\n  readonly decorative?: boolean;\n  readonly clip?: MarkClip;\n}\n\nexport interface PointZone {\n  readonly key: string;\n  readonly x: number;\n  readonly y: number;\n  readonly category: string;\n  readonly label: string;\n  readonly value: string;\n  readonly colour: string;\n}\n\nexport interface ValueLabel {\n  readonly key: string;\n  readonly x: number;\n  readonly y: number;\n  readonly width: number;\n  readonly text: string;\n}\n\nexport type LegendSide = \"top\" | \"bottom\";\n\nexport type LegendShape = \"square\" | \"dot\" | \"line\";\n\nexport type TooltipIndicator = \"dot\" | \"line\" | \"none\" | \"icon\";\n\nexport interface PaintContext {\n  readonly series: ReadonlyArray<ChartSeries>;\n  readonly labels: ReadonlyArray<string>;\n  readonly box: ChartBox;\n  readonly range: ChartRange;\n  readonly palette: ReadonlyArray<string>;\n  readonly origin: Option.Option<Point>;\n  readonly view?: ChartView;\n  readonly hovered?: Option.Option<number>;\n  readonly reveal?: number;\n}\n\nexport interface ChartFrameProps extends Omit<Props, \"children\"> {\n  readonly series: ReadonlyArray<ChartSeries>;\n  readonly labels: ReadonlyArray<string>;\n  readonly width?: number;\n  readonly height?: number;\n  readonly legend?: boolean | LegendSide;\n  readonly legendShape?: LegendShape;\n  readonly axis?: boolean;\n  readonly grid?: boolean;\n  readonly xLabels?: boolean;\n  readonly ticks?: number;\n  readonly colors?: ReadonlyArray<string>;\n  readonly tooltip?: boolean;\n  readonly tooltipIndicator?: TooltipIndicator;\n  readonly tooltipIcons?: ReadonlyArray<IconName>;\n  readonly labelFormat?: (label: string) => string;\n  readonly activeBand?: boolean;\n  readonly crosshair?: boolean;\n  readonly domain?: readonly [number, number];\n  readonly view?: readonly [number, number];\n  readonly zoom?: boolean;\n  readonly onViewChange?: (view: readonly [number, number]) => void;\n  readonly brush?: boolean;\n  readonly onDomainChange?: (domain: readonly [number, number]) => void;\n  readonly domainLabels?: ReadonlyArray<string>;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly valueFormat?: (value: number) => string;\n  readonly valueLabels?: (context: PaintContext) => ReadonlyArray<ValueLabel>;\n  readonly range?: ChartRange;\n  readonly paint: (context: PaintContext) => ReadonlyArray<PaintLayer>;\n  readonly gradients?: (context: PaintContext) => ReadonlyArray<ChartGradient>;\n  readonly render?: ChartRender;\n  readonly config?: ChartConfig;\n  readonly legendNode?: (side: LegendSide) => ReactNode;\n  readonly overlay?: (context: PaintContext) => ReactNode;\n  readonly className?: string;\n  readonly testId?: string;\n}\n\nexport const ChartMask = ({\n  chartTestId,\n  layer,\n  label,\n  description,\n  colour,\n  box,\n}: {\n  readonly chartTestId: string | undefined;\n  readonly layer: PaintLayer;\n  readonly label: string;\n  readonly description: string;\n  readonly colour: string;\n  readonly box: ChartBox;\n}) => {\n  const decorative = layer.decorative === true;\n  return (\n    <svg\n      source={chartMarkup(layer.marks, box, layer.clip)}\n      role={decorative ? undefined : \"img\"}\n      aria-label={decorative ? undefined : label}\n      aria-description={decorative ? undefined : description}\n      testId={\n        chartTestId === undefined\n          ? undefined\n          : layer.kind === undefined\n            ? `${chartTestId}-layer-${layer.key}`\n            : `${chartTestId}-layer-${layer.key}-${layer.kind}`\n      }\n      style={{\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: box.width,\n        height: box.height,\n        color: colour,\n        opacity: layer.opacity,\n        pointerEvents: \"none\",\n      }}\n    />\n  );\n};\n\nexport const paletteOf = (\n  theme: ReturnType<typeof useTheme>,\n  colors: ReadonlyArray<string> | undefined,\n): ReadonlyArray<string> => {\n  const chosen = Arr.filter(colors ?? [], (colour) => colour.trim().length > 0);\n  return chosen.length > 0 ? chosen : seriesColors(theme);\n};\n\nexport const seriesColourOf = (\n  palette: ReadonlyArray<string>,\n  series: ChartSeries,\n  index: number,\n): string =>\n  Option.match(\n    Option.filter(\n      Option.fromNullishOr(series.color),\n      (colour) => colour.trim().length > 0,\n    ),\n    {\n      onNone: () => palette[index % Math.max(1, palette.length)] ?? \"#000000\",\n      onSome: (colour) => colour,\n    },\n  );\n\nconst tooltipIcon = (\n  icons: ReadonlyArray<IconName> | undefined,\n  index: number,\n): IconName => {\n  const name = icons?.[index];\n  return name !== undefined && isIconName(name) ? name : \"circle\";\n};\n\nconst pointerIndex = (\n  windowX: number | undefined,\n  origin: Option.Option<Point>,\n  box: ChartBox,\n  count: number,\n): number =>\n  Option.match(origin, {\n    onNone: () => 0,\n    onSome: (base) => {\n      const x = windowX ?? base.x;\n      return Math.max(\n        0,\n        Math.min(count, ((x - base.x) / Math.max(1, box.width)) * count),\n      );\n    },\n  });\n\nconst slotsOf = (\n  series: ReadonlyArray<ChartSeries>,\n  labels: ReadonlyArray<string>,\n): number =>\n  labels.length > 0\n    ? labels.length\n    : Math.max(\n        1,\n        Arr.reduce(series, 0, (most, one) => Math.max(most, one.values.length)),\n      );\n\nexport const ChartFrame = ({\n  series,\n  labels,\n  width = DEFAULT_CHART_WIDTH,\n  height = DEFAULT_CHART_HEIGHT,\n  legend,\n  axis = true,\n  grid = true,\n  xLabels = true,\n  ticks = 4,\n  colors,\n  tooltip = true,\n  tooltipIndicator = \"dot\",\n  tooltipIcons,\n  labelFormat,\n  activeBand = false,\n  crosshair = false,\n  domain,\n  view,\n  zoom = false,\n  onViewChange,\n  brush = false,\n  onDomainChange,\n  domainLabels,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  valueFormat,\n  valueLabels,\n  range,\n  paint,\n  gradients,\n  render = \"document\",\n  config,\n  legendNode,\n  overlay,\n  className,\n  testId,\n  style,\n  ...rest\n}: ChartFrameProps) => {\n  const theme = useTheme();\n  const palette = paletteOf(theme, colors);\n  const mode = themeModeOf(theme);\n  const colourOf = (series: ChartSeries, index: number): string =>\n    Option.getOrElse(configSeriesColour(config, series.key, mode), () =>\n      seriesColourOf(palette, series, index),\n    );\n  const box = chartBox(width, height, {\n    width: DEFAULT_CHART_WIDTH,\n    height: DEFAULT_CHART_HEIGHT,\n  });\n  const targetRows = Arr.map(series, (one) => ({\n    ...one,\n    values: Arr.take(one.values, Math.max(1, labels.length)),\n  }));\n  const rows = useAnimatedSeries(targetRows, animate, animationDuration);\n  const reveal = useMountReveal(animate, animationDuration);\n  const categories = labels;\n  const shownView = Option.getOrUndefined(chartView(categories.length, view));\n  const labelView = shownView ?? {\n    start: 0,\n    end: Math.max(1, categories.length),\n  };\n  const measured = range ?? chartRange(targetRows, categories.length);\n  const tickValues = rangeTicks(measured, ticks);\n  const tickStep =\n    tickValues.length > 1\n      ? (measured.max - measured.min) / (tickValues.length - 1)\n      : measured.max;\n  const showLegend = legend === undefined ? rows.length > 1 : legend !== false;\n  const side: LegendSide = legend === \"top\" ? \"top\" : \"bottom\";\n  const slot = box.width / slotsOf(rows, categories);\n  const [hovered, setHovered] = useState<Option.Option<number>>(Option.none());\n  const [pointer, setPointer] = useState<Option.Option<Point>>(Option.none());\n  const selectFrom = useRef<Option.Option<number>>(Option.none());\n  const [selectTo, setSelectTo] = useState<Option.Option<number>>(\n    Option.none(),\n  );\n  const [origin, setOrigin] = useState<Option.Option<Point>>(Option.none());\n  const format = valueFormat ?? valueLabel;\n  const context: PaintContext = {\n    series: rows,\n    labels: categories,\n    box,\n    range: measured,\n    palette,\n    origin,\n    view: shownView,\n    hovered,\n    reveal,\n  };\n  const layers = paint(context);\n  const legendBody =\n    legendNode === undefined || !showLegend ? undefined : legendNode(side);\n  return (\n    <div\n      {...rest}\n      role=\"group\"\n      testId={testId}\n      style={mergeStyles(recipeStyle(cn(CHART_ROOT, className), theme), style)}\n    >\n      {side === \"top\" ? legendBody : undefined}\n      <div style={{ display: \"flex\", flexDirection: \"row\" }}>\n        {axis ? (\n          <div\n            testId={testId === undefined ? undefined : `${testId}-axis`}\n            style={{\n              ...recipeStyle(CHART_AXIS, theme),\n              width: CHART_AXIS_WIDTH,\n              height: box.height,\n            }}\n          >\n            {Arr.map(Arr.reverse(tickValues), (value) => (\n              <text\n                key={`tick-${value}`}\n                testId={\n                  testId === undefined ? undefined : `${testId}-tick-${value}`\n                }\n                style={{\n                  ...recipeStyle(CHART_AXIS_LABEL, theme),\n                  textAlign: \"right\",\n                }}\n              >\n                {tickLabel(value, tickStep)}\n              </text>\n            ))}\n          </div>\n        ) : undefined}\n        <div\n          testId={testId === undefined ? undefined : `${testId}-plot`}\n          onLayout={(event) =>\n            setOrigin(\n              Option.all([\n                Option.fromNullishOr(event.x),\n                Option.fromNullishOr(event.y),\n              ]).pipe(Option.map(([x, y]) => ({ x, y }))),\n            )\n          }\n          style={{\n            ...recipeStyle(CHART_PLOT, theme),\n            width: box.width,\n            height: box.height,\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: 0,\n              width: box.width,\n              height: box.height,\n              overflow: \"hidden\",\n            }}\n          >\n            {activeBand\n              ? Option.match(hovered, {\n                  onNone: () => null,\n                  onSome: (index) => (\n                    <div\n                      style={{\n                        position: \"absolute\",\n                        left: slot * index,\n                        top: 0,\n                        width: slot,\n                        height: box.height,\n                        backgroundColor: themeColors(theme).muted,\n                        opacity: 0.5,\n                        pointerEvents: \"none\",\n                      }}\n                    />\n                  ),\n                })\n              : undefined}\n            {crosshair\n              ? Option.match(hovered, {\n                  onNone: () => null,\n                  onSome: (index) => (\n                    <div\n                      testId={\n                        testId === undefined ? undefined : `${testId}-crosshair`\n                      }\n                      style={{\n                        position: \"absolute\",\n                        left: slot * index + slot / 2,\n                        top: 0,\n                        width: 1,\n                        height: box.height,\n                        backgroundColor:\n                          themeColors(theme).borderStrong ??\n                          themeColors(theme).border,\n                        opacity: 0.8,\n                        pointerEvents: \"none\",\n                      }}\n                    />\n                  ),\n                })\n              : undefined}\n            <ChartPlot\n              box={box}\n              layers={layers}\n              colours={Arr.map(layers, (layer) =>\n                colourOf(layer.series, layer.seriesIndex),\n              )}\n              gridColour={themeColors(theme).border ?? \"#000000\"}\n              grid={\n                grid\n                  ? {\n                      key: \"grid\",\n                      kind: \"grid\",\n                      series: { key: \"grid\", label: \"grid\", values: [] },\n                      seriesIndex: 0,\n                      decorative: true,\n                      opacity: 0.55,\n                      marks: Arr.map(tickValues, (value) => ({\n                        kind: \"rect\" as const,\n                        x: 0,\n                        y:\n                          box.height -\n                          ((value - measured.min) /\n                            Math.max(1e-9, measured.max - measured.min)) *\n                            box.height,\n                        width: box.width,\n                        height: 1,\n                      })),\n                    }\n                  : undefined\n              }\n              render={render}\n              gradients={gradients === undefined ? [] : gradients(context)}\n              testId={testId}\n              labels={Arr.map(layers, (layer) =>\n                seriesName(layer.series.label, layer.series.key),\n              )}\n              descriptions={Arr.map(layers, (layer) =>\n                seriesDescription(layer.series.values, categories),\n              )}\n            />\n          </div>\n          {valueLabels === undefined ? undefined : (\n            <ValueLabels\n              labels={valueLabels(context)}\n              theme={theme}\n              testId={testId}\n            />\n          )}\n          {zoom ? (\n            <div\n              testId={testId === undefined ? undefined : `${testId}-gesture`}\n              onMouseDown={(event) => {\n                const at = pointerIndex(\n                  event.x,\n                  origin,\n                  box,\n                  categories.length,\n                );\n                selectFrom.current = Option.some(at);\n                setSelectTo(Option.some(at));\n              }}\n              onMouseMove={(event) => {\n                const index = pointerIndex(\n                  event.x,\n                  origin,\n                  box,\n                  categories.length,\n                );\n                setHovered(\n                  Option.some(\n                    Math.max(\n                      0,\n                      Math.min(\n                        Math.max(0, categories.length - 1),\n                        Math.floor(index),\n                      ),\n                    ),\n                  ),\n                );\n                setPointer(\n                  Option.all([\n                    Option.fromNullishOr(event.x),\n                    Option.fromNullishOr(event.y),\n                  ]).pipe(Option.map(([x, y]) => ({ x, y }))),\n                );\n                if (Option.isSome(selectFrom.current)) {\n                  setSelectTo(Option.some(index));\n                }\n              }}\n              onMouseUp={() => {\n                Option.match(selectFrom.current, {\n                  onNone: () => {},\n                  onSome: (from) =>\n                    Option.match(selectTo, {\n                      onNone: () => {},\n                      onSome: (to) => {\n                        if (Math.abs(to - from) >= 0.5) {\n                          onViewChange?.([\n                            Math.min(from, to),\n                            Math.max(from, to),\n                          ]);\n                        }\n                      },\n                    }),\n                });\n                selectFrom.current = Option.none();\n                setSelectTo(Option.none());\n              }}\n              onScroll={(event) => {\n                const delta = event.deltaY ?? 0;\n                if (delta === 0) return;\n                const span = labelView.end - labelView.start;\n                const factor = Math.max(\n                  0.25,\n                  Math.min(4, Math.exp(delta * 0.01)),\n                );\n                const centre = labelView.start + span / 2;\n                const next = span * factor;\n                onViewChange?.([\n                  Math.max(\n                    0,\n                    Math.min(categories.length - 1, centre - next / 2),\n                  ),\n                  Math.min(\n                    categories.length,\n                    Math.max(centre - next / 2 + 1, centre + next / 2),\n                  ),\n                ]);\n              }}\n              onMouseLeave={() => {\n                setHovered(Option.none());\n                setPointer(Option.none());\n                selectFrom.current = Option.none();\n                setSelectTo(Option.none());\n              }}\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                width: box.width,\n                height: box.height,\n              }}\n            >\n              {Option.match(selectFrom.current, {\n                onNone: () => null,\n                onSome: (from) =>\n                  Option.match(selectTo, {\n                    onNone: () => null,\n                    onSome: (to) => {\n                      const low = Math.min(from, to);\n                      const high = Math.max(from, to);\n                      const width = box.width / Math.max(1, categories.length);\n                      return (\n                        <div\n                          style={{\n                            position: \"absolute\",\n                            left: low * width,\n                            top: 0,\n                            width: Math.max(1, (high - low) * width),\n                            height: box.height,\n                            backgroundColor: themeColors(theme).accent,\n                            opacity: 0.25,\n                            pointerEvents: \"none\",\n                          }}\n                        />\n                      );\n                    },\n                  }),\n              })}\n            </div>\n          ) : tooltip && rows.length > 0 && categories.length > 0 ? (\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                width: box.width,\n                height: box.height,\n                display: \"flex\",\n                flexDirection: \"row\",\n              }}\n            >\n              {Arr.makeBy(categories.length, (index) => (\n                <div\n                  key={`zone-${index}`}\n                  testId={\n                    testId === undefined ? undefined : `${testId}-zone-${index}`\n                  }\n                  onMouseEnter={() => setHovered(Option.some(index))}\n                  onMouseMove={(event) =>\n                    setPointer(\n                      Option.all([\n                        Option.fromNullishOr(event.x),\n                        Option.fromNullishOr(event.y),\n                      ]).pipe(Option.map(([x, y]) => ({ x, y }))),\n                    )\n                  }\n                  onMouseLeave={() => {\n                    setHovered(Option.none());\n                    setPointer(Option.none());\n                  }}\n                  style={{\n                    ...recipeStyle(CHART_TOOLTIP_ZONE, theme),\n                    width: slot,\n                    height: box.height,\n                  }}\n                />\n              ))}\n            </div>\n          ) : undefined}\n          {tooltip\n            ? Option.match(hovered, {\n                onNone: () => null,\n                onSome: (index) => (\n                  <ChartTooltip\n                    series={targetRows}\n                    labels={categories}\n                    palette={palette}\n                    theme={theme}\n                    index={index}\n                    slot={slot}\n                    box={box}\n                    pointer={Option.flatMap(pointer, (at) =>\n                      Option.map(origin, (base) => ({\n                        x: at.x - base.x,\n                        y: at.y - base.y,\n                      })),\n                    )}\n                    format={format}\n                    indicator={tooltipIndicator}\n                    tooltipIcons={tooltipIcons}\n                    labelFormat={labelFormat}\n                    testId={testId}\n                  />\n                ),\n              })\n            : undefined}\n          {overlay === undefined ? undefined : overlay(context)}\n        </div>\n      </div>\n      {xLabels && view !== undefined ? (\n        <div\n          testId={testId === undefined ? undefined : `${testId}-categories`}\n          style={{\n            position: \"relative\",\n            height: 16,\n            width: box.width,\n            marginLeft: axis ? CHART_AXIS_WIDTH : 0,\n            overflow: \"hidden\",\n          }}\n        >\n          {Arr.map(categories, (label, index) => {\n            const width =\n              box.width / Math.max(1, labelView.end - labelView.start);\n            return (\n              <text\n                key={`category-${index}`}\n                testId={\n                  testId === undefined ? undefined : `${testId}-label-${index}`\n                }\n                style={{\n                  ...recipeStyle(CHART_CATEGORY_LABEL, theme),\n                  position: \"absolute\",\n                  left: (index - labelView.start) * width,\n                  top: 0,\n                  width,\n                  textAlign: \"center\",\n                }}\n              >\n                {label}\n              </text>\n            );\n          })}\n        </div>\n      ) : xLabels ? (\n        <div\n          testId={testId === undefined ? undefined : `${testId}-categories`}\n          style={{\n            ...recipeStyle(CHART_CATEGORIES, theme),\n            paddingLeft: axis ? CHART_AXIS_WIDTH : 0,\n          }}\n        >\n          {Arr.map(categories, (label, index) => (\n            <div\n              key={`category-${index}`}\n              testId={\n                testId === undefined ? undefined : `${testId}-label-${index}`\n              }\n              style={{ ...recipeStyle(CHART_CATEGORY, theme), width: slot }}\n            >\n              <text\n                style={{\n                  ...recipeStyle(CHART_CATEGORY_LABEL, theme),\n                  width: slot,\n                  textAlign: \"center\",\n                }}\n              >\n                {label}\n              </text>\n            </div>\n          ))}\n        </div>\n      ) : undefined}\n      {zoom ? (\n        <ChartZoom\n          length={categories.length}\n          view={shownView}\n          theme={theme}\n          onViewChange={onViewChange}\n          width={box.width}\n          padding={axis ? CHART_AXIS_WIDTH : 0}\n          testId={testId}\n        />\n      ) : undefined}\n      {brush ? (\n        <ChartBrush\n          labels={domainLabels ?? categories}\n          domain={domain}\n          theme={theme}\n          onDomainChange={onDomainChange}\n          width={box.width}\n          padding={axis ? CHART_AXIS_WIDTH : 0}\n          testId={testId}\n        />\n      ) : undefined}\n      {side === \"bottom\" ? legendBody : undefined}\n    </div>\n  );\n};\n\nexport const PointTooltip = ({\n  zones,\n  box,\n  origin,\n  testId,\n}: {\n  readonly zones: ReadonlyArray<PointZone>;\n  readonly box: ChartBox;\n  readonly origin: Option.Option<Point>;\n  readonly testId: string | undefined;\n}) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const [active, setActive] = useState<Option.Option<number>>(Option.none());\n  const [pointer, setPointer] = useState<Option.Option<Point>>(Option.none());\n  const width = 128;\n  return (\n    <>\n      {Arr.map(zones, (zone, index) => (\n        <div\n          key={`${zone.key}-${index}`}\n          testId={\n            testId === undefined\n              ? undefined\n              : `${testId}-point-${zone.key}-${index}`\n          }\n          onMouseEnter={() => setActive(Option.some(index))}\n          onMouseMove={(event) =>\n            setPointer(\n              Option.all([\n                Option.fromNullishOr(event.x),\n                Option.fromNullishOr(event.y),\n                origin,\n              ]).pipe(\n                Option.map(([x, y, base]) => ({\n                  x: x - base.x,\n                  y: y - base.y,\n                })),\n              ),\n            )\n          }\n          onMouseLeave={() => {\n            setActive(Option.none());\n            setPointer(Option.none());\n          }}\n          style={{\n            position: \"absolute\",\n            left: zone.x - 9,\n            top: zone.y - 9,\n            width: 18,\n            height: 18,\n            borderRadius: 9,\n          }}\n        />\n      ))}\n      {Option.match(active, {\n        onNone: () => null,\n        onSome: (index) => {\n          const zone = zones[index];\n          if (zone === undefined) return null;\n          return (\n            <div\n              testId={testId === undefined ? undefined : `${testId}-point-tip`}\n              style={{\n                ...recipeStyle(CHART_TOOLTIP, theme),\n                position: \"absolute\",\n                left: Option.match(pointer, {\n                  onNone: () => Math.max(0, zone.x + 10),\n                  onSome: (at) =>\n                    Math.max(0, Math.min(box.width - 8, at.x + 12)),\n                }),\n                top: Option.match(pointer, {\n                  onNone: () => Math.max(0, zone.y - 40),\n                  onSome: (at) => Math.max(0, at.y + 12),\n                }),\n                width,\n                pointerEvents: \"none\",\n              }}\n            >\n              <text\n                style={{\n                  ...recipeStyle(CHART_TOOLTIP_HEAD, theme),\n                  ...textStyle(theme, \"caption\"),\n                  color: colors.mutedForeground,\n                }}\n              >\n                {zone.category}\n              </text>\n              <div style={recipeStyle(CHART_TOOLTIP_ROW, theme)}>\n                <div\n                  style={{\n                    width: 6,\n                    height: 6,\n                    borderRadius: 3,\n                    backgroundColor: zone.colour,\n                  }}\n                />\n                <text\n                  style={{\n                    ...textStyle(theme, \"caption\"),\n                    color: colors.mutedForeground,\n                  }}\n                >\n                  {zone.label}\n                </text>\n                <text\n                  style={{\n                    ...recipeStyle(CHART_TOOLTIP_VALUE, theme),\n                    ...textStyle(theme, \"caption\"),\n                    color: colors.foreground,\n                  }}\n                >\n                  {zone.value}\n                </text>\n              </div>\n            </div>\n          );\n        },\n      })}\n    </>\n  );\n};\n\n/** The zoom strip: drag to pan, wheel to zoom, and reset to show everything. */\nconst ChartZoom = ({\n  length,\n  view,\n  theme,\n  onViewChange,\n  width,\n  padding,\n  testId,\n}: {\n  readonly length: number;\n  readonly view: ChartView | undefined;\n  readonly theme: ReturnType<typeof useTheme>;\n  readonly onViewChange:\n    | ((view: readonly [number, number]) => void)\n    | undefined;\n  readonly width: number;\n  readonly padding: number;\n  readonly testId: string | undefined;\n}) => {\n  const span = view === undefined ? Math.max(1, length) : view.end - view.start;\n  const start = view === undefined ? 0 : view.start;\n  const [anchor, setAnchor] = useState<\n    Option.Option<{ readonly x: number; readonly start: number }>\n  >(Option.none());\n  const apply = (nextStart: number, nextSpan: number): void => {\n    const spanNow = Math.max(1, Math.min(length, nextSpan));\n    const startNow = Math.max(0, Math.min(length - spanNow, nextStart));\n    onViewChange?.([startNow, startNow + spanNow]);\n  };\n  return (\n    <div style={{ marginLeft: padding, width }}>\n      <div\n        testId={testId === undefined ? undefined : `${testId}-zoom`}\n        onMouseDown={(event) =>\n          setAnchor(\n            Option.map(Option.fromNullishOr(event.x), (x) => ({\n              x,\n              start,\n            })),\n          )\n        }\n        onMouseLeave={() => setAnchor(Option.none())}\n        onMouseMove={(event) => {\n          if (Option.isNone(anchor)) return;\n          Option.match(anchor, {\n            onNone: () => {},\n            onSome: (base) => {\n              const delta = ((event.x ?? base.x) - base.x) / Math.max(1, width);\n              apply(base.start - delta * span, span);\n            },\n          });\n        }}\n        onMouseUp={() => setAnchor(Option.none())}\n        onScroll={(event) => {\n          const delta = event.deltaY ?? 0;\n          if (delta === 0) return;\n          const factor = Math.max(0.25, Math.min(4, Math.exp(delta * 0.01)));\n          const centre = start + span / 2;\n          const nextSpan = span * factor;\n          apply(centre - nextSpan / 2, nextSpan);\n        }}\n        style={{\n          position: \"relative\",\n          width,\n          height: 16,\n          backgroundColor: themeColors(theme).muted,\n          borderRadius: 3,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            top: 0,\n            bottom: 0,\n            left: (start / Math.max(1, length)) * width,\n            width: (span / Math.max(1, length)) * width,\n            backgroundColor: themeColors(theme).accent,\n            opacity: 0.4,\n            pointerEvents: \"none\",\n          }}\n        />\n      </div>\n      <div\n        role=\"button\"\n        testId={testId === undefined ? undefined : `${testId}-zoom-reset`}\n        onClick={() => {\n          onViewChange?.([0, Math.max(1, length)]);\n        }}\n        style={{\n          marginTop: 2,\n          fontSize: 11,\n          color: themeColors(theme).mutedForeground,\n        }}\n      >\n        Reset\n      </div>\n    </div>\n  );\n};\n\nconst ChartBrush = ({\n  labels,\n  domain,\n  theme,\n  onDomainChange,\n  width,\n  padding,\n  testId,\n}: {\n  readonly labels: ReadonlyArray<string>;\n  readonly domain: readonly [number, number] | undefined;\n  readonly theme: ReturnType<typeof useTheme>;\n  readonly onDomainChange:\n    | ((domain: readonly [number, number]) => void)\n    | undefined;\n  readonly width: number;\n  readonly padding: number;\n  readonly testId: string | undefined;\n}) => {\n  const colors = themeColors(theme);\n  const [origin, setOrigin] = useState<Option.Option<Point>>(Option.none());\n  const drag = useRef<Option.Option<number>>(Option.none());\n  const [edge, setEdge] = useState<Option.Option<number>>(Option.none());\n  const [, redraw] = useState(0);\n  const count = Math.max(1, labels.length);\n  const committed = Option.getOrElse(\n    chartWindow(labels.length, domain),\n    () => ({\n      start: 0,\n      end: Math.max(1, labels.length),\n    }),\n  );\n  const selected = Option.match(drag.current, {\n    onNone: () => committed,\n    onSome: (start) =>\n      Option.match(edge, {\n        onNone: () => ({ start, end: start + 1 }),\n        onSome: (end) => ({\n          start: Math.min(start, end),\n          end: Math.max(start, end) + 1,\n        }),\n      }),\n  });\n  const indexAt = (windowX: number): number =>\n    Option.match(origin, {\n      onNone: () => 0,\n      onSome: (base) => {\n        const ratio = (windowX - base.x) / Math.max(1, width);\n        return Math.max(0, Math.min(count - 1, Math.floor(ratio * count)));\n      },\n    });\n  const report = (start: number, end: number): void => {\n    onDomainChange?.([Math.min(start, end), Math.max(start, end) + 1]);\n  };\n  const left = (selected.start / count) * width;\n  const selectedWidth = Math.max(\n    2,\n    ((selected.end - selected.start) / count) * width,\n  );\n  return (\n    <div\n      testId={testId === undefined ? undefined : `${testId}-brush`}\n      onLayout={(event) =>\n        setOrigin(\n          Option.all([\n            Option.fromNullishOr(event.x),\n            Option.fromNullishOr(event.y),\n          ]).pipe(Option.map(([x, y]) => ({ x, y }))),\n        )\n      }\n      onMouseDown={(event) => {\n        const at = indexAt(event.x ?? 0);\n        drag.current = Option.some(at);\n        setEdge(Option.some(at));\n        redraw((tick) => tick + 1);\n        report(at, at);\n      }}\n      onMouseMove={(event) => {\n        if (Option.isNone(drag.current)) return;\n        const at = indexAt(event.x ?? 0);\n        setEdge(Option.some(at));\n        Option.match(drag.current, {\n          onNone: () => {},\n          onSome: (start) => report(start, at),\n        });\n      }}\n      onMouseUp={() => {\n        drag.current = Option.none();\n        setEdge(Option.none());\n        redraw((tick) => tick + 1);\n      }}\n      onMouseLeave={() => {\n        drag.current = Option.none();\n        setEdge(Option.none());\n        redraw((tick) => tick + 1);\n      }}\n      style={{\n        ...recipeStyle(CHART_BRUSH, theme),\n        width,\n        height: 16,\n        marginLeft: padding,\n      }}\n    >\n      <div style={recipeStyle(CHART_BRUSH_TRACK, theme)}>\n        <div\n          testId={\n            testId === undefined ? undefined : `${testId}-brush-selection`\n          }\n          style={{\n            ...recipeStyle(CHART_BRUSH_BAND, theme),\n            position: \"absolute\",\n            left,\n            width: selectedWidth,\n            backgroundColor: colors.accent,\n          }}\n        />\n        <div\n          style={{\n            ...recipeStyle(CHART_BRUSH_HANDLE, theme),\n            position: \"absolute\",\n            left,\n            backgroundColor: colors.primary,\n          }}\n        />\n        <div\n          style={{\n            ...recipeStyle(CHART_BRUSH_HANDLE, theme),\n            position: \"absolute\",\n            left: left + selectedWidth - 4,\n            backgroundColor: colors.primary,\n          }}\n        />\n      </div>\n    </div>\n  );\n};\n\nconst ChartTooltip = ({\n  series,\n  labels,\n  palette,\n  theme,\n  index,\n  slot,\n  box,\n  pointer,\n  format,\n  indicator,\n  tooltipIcons,\n  labelFormat,\n  testId,\n}: {\n  readonly series: ReadonlyArray<ChartSeries>;\n  readonly labels: ReadonlyArray<string>;\n  readonly palette: ReadonlyArray<string>;\n  readonly theme: ReturnType<typeof useTheme>;\n  readonly index: number;\n  readonly slot: number;\n  readonly box: ChartBox;\n  /** The pointer, in plot coordinates, when the runtime reported one. */\n  readonly pointer: Option.Option<Point>;\n  readonly format: (value: number) => string;\n  readonly indicator: TooltipIndicator;\n  readonly tooltipIcons: ReadonlyArray<IconName> | undefined;\n  readonly labelFormat: ((label: string) => string) | undefined;\n  readonly testId: string | undefined;\n}) => {\n  const colors = themeColors(theme);\n  const width = 132;\n  const column = slot * index + slot / 2 - width / 2;\n  const left = Option.match(pointer, {\n    onNone: () => Math.max(0, Math.min(box.width - width, column)),\n    onSome: (at) => Math.max(0, Math.min(box.width - width, at.x + 12)),\n  });\n  const top = Option.match(pointer, {\n    onNone: () => 0,\n    onSome: (at) => Math.max(0, Math.min(box.height - 64, at.y + 12)),\n  });\n  return (\n    <div\n      testId={testId === undefined ? undefined : `${testId}-tooltip`}\n      style={{\n        ...recipeStyle(CHART_TOOLTIP, theme),\n        position: \"absolute\",\n        left,\n        top,\n        width,\n        pointerEvents: \"none\",\n      }}\n    >\n      <text\n        style={{\n          ...recipeStyle(CHART_TOOLTIP_HEAD, theme),\n          ...textStyle(theme, \"caption\"),\n          color: colors.mutedForeground,\n        }}\n      >\n        {Option.match(Option.fromNullishOr(labels[index]), {\n          onNone: () => \"\",\n          onSome: (label) =>\n            labelFormat === undefined ? label : labelFormat(label),\n        })}\n      </text>\n      {Arr.map(series, (one, seriesIndex) => (\n        <div\n          key={one.key}\n          testId={\n            testId === undefined ? undefined : `${testId}-tooltip-${one.key}`\n          }\n          style={recipeStyle(CHART_TOOLTIP_ROW, theme)}\n        >\n          {indicator === \"none\" ? undefined : indicator === \"icon\" ? (\n            <Icon\n              color={seriesColourOf(palette, one, seriesIndex)}\n              name={tooltipIcon(tooltipIcons, seriesIndex)}\n              size={12}\n              testId={\n                testId === undefined\n                  ? undefined\n                  : `${testId}-tooltip-icon-${one.key}`\n              }\n            />\n          ) : (\n            <div\n              testId={\n                testId === undefined\n                  ? undefined\n                  : `${testId}-tooltip-indicator-${one.key}`\n              }\n              style={{\n                width: indicator === \"line\" ? 10 : 6,\n                height: indicator === \"line\" ? 2 : 6,\n                borderRadius: indicator === \"line\" ? 1 : 3,\n                backgroundColor: seriesColourOf(palette, one, seriesIndex),\n              }}\n            />\n          )}\n          <text\n            style={{\n              ...textStyle(theme, \"caption\"),\n              color: colors.mutedForeground,\n            }}\n          >\n            {seriesName(one.label, one.key)}\n          </text>\n          <text\n            style={{\n              ...recipeStyle(CHART_TOOLTIP_VALUE, theme),\n              ...textStyle(theme, \"caption\"),\n              color: colors.foreground,\n            }}\n          >\n            {format(one.values[index] ?? 0)}\n          </text>\n        </div>\n      ))}\n    </div>\n  );\n};\n\n/** The values a bar chart paints above its bars. */\nconst ValueLabels = ({\n  labels,\n  theme,\n  testId,\n}: {\n  readonly labels: ReadonlyArray<ValueLabel>;\n  readonly theme: ReturnType<typeof useTheme>;\n  readonly testId: string | undefined;\n}) => {\n  const colors = themeColors(theme);\n  return Arr.map(labels, (label) => (\n    <text\n      key={`${label.key}-${label.x}`}\n      testId={testId === undefined ? undefined : `${testId}-value-${label.key}`}\n      style={{\n        ...recipeStyle(CHART_VALUE_LABEL, theme),\n        position: \"absolute\",\n        left: label.x,\n        top: label.y,\n        width: label.width,\n        textAlign: \"center\",\n        color: colors.mutedForeground,\n      }}\n    >\n      {label.text}\n    </text>\n  ));\n};\n"
    },
    {
      "path": "packages/data/src/chart/geometry.ts",
      "target": "@ui/gpuixui/data/chart/geometry.ts",
      "type": "registry:lib",
      "content": "import { Match } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport type { ChartBox, ChartMark, ChartSeries } from \"./logic.js\";\n\nexport interface Point {\n  readonly x: number;\n  readonly y: number;\n}\n\nexport type Curve = \"linear\" | \"smooth\" | \"step\";\n\nexport interface ChartRange {\n  readonly min: number;\n  readonly max: number;\n}\n\nexport const finite = (value: number): number =>\n  Number.isFinite(value) ? value : 0;\n\nconst slotCount = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n): number => {\n  if (categories !== undefined && Number.isFinite(categories)) {\n    return Math.max(0, Math.floor(categories));\n  }\n  return Arr.reduce(series, 0, (most, one) =>\n    Math.max(most, one.values.length),\n  );\n};\n\nconst paintedValues = (\n  values: ReadonlyArray<number>,\n  categories?: number,\n): ReadonlyArray<number> =>\n  categories === undefined || !Number.isFinite(categories)\n    ? values\n    : Arr.take(values, Math.max(0, Math.floor(categories)));\n\nexport const niceBound = (raw: number): number => {\n  const value = Math.abs(finite(raw));\n  if (value === 0) return 0;\n  const magnitude = 10 ** Math.floor(Math.log10(value));\n  const step = value / magnitude;\n  const nice = step <= 1 ? 1 : step <= 2 ? 2 : step <= 5 ? 5 : 10;\n  return (raw < 0 ? -1 : 1) * nice * magnitude;\n};\n\nexport const chartRange = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n  stacked = false,\n): ChartRange => {\n  const values = Arr.flatten(\n    Arr.map(series, (one) => paintedValues(one.values, categories)),\n  );\n  const totals =\n    stacked && series.length > 1\n      ? [\n          Arr.reduce(\n            Arr.map(\n              Arr.makeBy(slotCount(series, categories), (index) => index),\n              (index) =>\n                Arr.reduce(\n                  series,\n                  0,\n                  (sum, one) =>\n                    sum + Math.max(0, finite(one.values[index] ?? 0)),\n                ),\n            ),\n            0,\n            Math.max,\n          ),\n        ]\n      : [];\n  const highest = (input: ReadonlyArray<number>): number =>\n    Arr.reduce(input, 0, (most, value) => Math.max(most, value));\n  const max = niceBound(Math.max(highest(values), highest(totals), 0));\n  const min = niceBound(\n    Arr.reduce(values, 0, (least, value) => Math.min(least, value)),\n  );\n  return { min: Math.min(0, min), max: Math.max(max > 0 ? max : 1, 0) };\n};\n\nexport const scaleY = (\n  value: number,\n  box: ChartBox,\n  range: ChartRange,\n): number => {\n  const span = range.max - range.min;\n  if (span <= 0) return box.height;\n  return box.height - ((finite(value) - range.min) / span) * box.height;\n};\n\nexport const scaleX = (\n  value: number,\n  box: ChartBox,\n  range: ChartRange,\n): number => {\n  const span = range.max - range.min;\n  if (span <= 0) return 0;\n  return ((finite(value) - range.min) / span) * box.width;\n};\n\nexport const zeroY = (box: ChartBox, range: ChartRange): number =>\n  scaleY(0, box, range);\n\nexport const seriesPoints = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  range: ChartRange,\n  categories?: number,\n  view?: ChartView,\n): ReadonlyArray<Point> => {\n  const painted = paintedValues(values, categories);\n  const count = slotCount(\n    [{ key: \"\", label: \"\", values: painted }],\n    categories ?? painted.length,\n  );\n  const span = view === undefined ? count : view.end - view.start;\n  const slot = span <= 0 ? 0 : box.width / span;\n  const offset = view === undefined ? 0 : view.start;\n  return Arr.map(painted, (value, index) => ({\n    x: (index + 0.5 - offset) * slot,\n    y: scaleY(value, box, range),\n  }));\n};\n\nconst at = (value: number): string =>\n  `${Math.round((Number.isFinite(value) ? value : 0) * 100) / 100}`;\n\nconst pair = (point: Point): string => `${at(point.x)} ${at(point.y)}`;\n\nconst monotoneTangents = (\n  points: ReadonlyArray<Point>,\n): ReadonlyArray<number> => {\n  const count = points.length;\n  if (count < 2) return Arr.map(points, () => 0);\n  const run: number[] = [];\n  const slope: number[] = [];\n  for (let index = 0; index < count - 1; index += 1) {\n    const from = points[index];\n    const to = points[index + 1];\n    if (from === undefined || to === undefined) continue;\n    const step = to.x - from.x;\n    run[index] = step;\n    slope[index] = step === 0 ? 0 : (to.y - from.y) / step;\n  }\n  const tangent: number[] = Arr.map(points, () => 0);\n  tangent[0] = slope[0] ?? 0;\n  tangent[count - 1] = slope[count - 2] ?? 0;\n  for (let index = 1; index < count - 1; index += 1) {\n    const before = slope[index - 1] ?? 0;\n    const after = slope[index] ?? 0;\n    if (before * after <= 0) {\n      tangent[index] = 0;\n      continue;\n    }\n    const left = 2 * (run[index] ?? 1) + (run[index - 1] ?? 1);\n    const right = (run[index] ?? 1) + 2 * (run[index - 1] ?? 1);\n    tangent[index] = (left + right) / (left / before + right / after);\n  }\n  return tangent;\n};\n\nexport const curvePath = (\n  points: ReadonlyArray<Point>,\n  curve: Curve,\n): Option.Option<string> => {\n  const first = Arr.head(points);\n  if (Option.isNone(first)) return Option.none();\n  if (points.length === 1) {\n    return Option.some(`M${pair(first.value)}h1`);\n  }\n  const [head, ...rest] = points;\n  if (head === undefined) return Option.none();\n  const tangents = monotoneTangents(points);\n  const body = Arr.map(rest, (point, index) => {\n    const previous = points[index];\n    if (previous === undefined) return \"\";\n    return Match.value(curve).pipe(\n      Match.when(\"step\", () => `H${at(point.x)}V${at(point.y)}`),\n      Match.when(\"smooth\", () => {\n        const h = (point.x - previous.x) / 3;\n        return `C${pair({\n          x: previous.x + h,\n          y: previous.y + (tangents[index] ?? 0) * h,\n        })} ${pair({\n          x: point.x - h,\n          y: point.y - (tangents[index + 1] ?? 0) * h,\n        })} ${pair(point)}`;\n      }),\n      Match.when(\"linear\", () => `L${pair(point)}`),\n      Match.exhaustive,\n    );\n  });\n  return Option.some(`M${pair(head)}${Arr.join(body, \"\")}`);\n};\n\nexport const areaPath = (\n  points: ReadonlyArray<Point>,\n  box: ChartBox,\n  range: ChartRange,\n  curve: Curve,\n): Option.Option<string> =>\n  Option.map(curvePath(points, curve), (line) => {\n    const last = Arr.last(points);\n    const first = Arr.head(points);\n    if (Option.isNone(last) || Option.isNone(first)) return line;\n    const floor = zeroY(box, range);\n    return `${line}L${at(last.value.x)} ${at(floor)}L${at(first.value.x)} ${at(floor)}Z`;\n  });\n\nexport const clipArea = (\n  points: ReadonlyArray<Point>,\n  cut: number,\n  floor: number,\n): Option.Option<string> => {\n  const kept: Array<Point> = [];\n  Arr.forEach(points, (point, index) => {\n    const previous = points[index - 1];\n    if (previous !== undefined) {\n      const above = previous.y <= cut;\n      const below = point.y <= cut;\n      if (above !== below) {\n        const t = (cut - previous.y) / (point.y - previous.y);\n        kept.push({\n          x: previous.x + (point.x - previous.x) * t,\n          y: cut,\n        });\n      }\n    }\n    if (point.y <= cut) kept.push(point);\n  });\n  if (kept.length === 0) return Option.none();\n  const first = Arr.head(kept);\n  const last = Arr.last(kept);\n  if (Option.isNone(first) || Option.isNone(last)) return Option.none();\n  return Option.some(\n    `M${pair(first.value)}${Arr.join(\n      Arr.map(Arr.drop(kept, 1), (point) => `L${pair(point)}`),\n      \"\",\n    )}L${at(last.value.x)} ${at(floor)}L${at(first.value.x)} ${at(floor)}Z`,\n  );\n};\n\nexport const areaBands = (\n  points: ReadonlyArray<Point>,\n  box: ChartBox,\n  range: ChartRange,\n  bands: number,\n): ReadonlyArray<string> => {\n  const steps = Math.max(1, Math.min(6, Math.floor(bands)));\n  const top = 0;\n  const floor = zeroY(box, range);\n  const span = floor - top;\n  if (span <= 0) return [];\n  return Arr.flatMap(\n    Arr.makeBy(steps, (index) => index + 1),\n    (index) =>\n      Option.match(clipArea(points, top + (span * index) / steps, floor), {\n        onNone: () => [],\n        onSome: (d): ReadonlyArray<string> => [d],\n      }),\n  );\n};\n\nexport const pointMarks = (\n  points: ReadonlyArray<Point>,\n  radius: number,\n): ReadonlyArray<ChartMark> =>\n  Arr.map(\n    points,\n    (point): ChartMark => ({\n      kind: \"circle\",\n      cx: point.x,\n      cy: point.y,\n      r: Math.max(1, radius),\n    }),\n  );\n\nexport const emphasisedPointMarks = (\n  points: ReadonlyArray<Point>,\n  radius: number,\n  hovered: number | undefined,\n): ReadonlyArray<ChartMark> =>\n  Arr.map(\n    points,\n    (point, index): ChartMark => ({\n      kind: \"circle\",\n      cx: point.x,\n      cy: point.y,\n      r: Math.max(1, index === hovered ? radius * 2 : radius),\n    }),\n  );\n\nexport const barMarksInRange = (\n  series: ReadonlyArray<ChartSeries>,\n  box: ChartBox,\n  range: ChartRange,\n  categories?: number,\n  radius = 0,\n  view?: ChartView,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const count = slotCount(series, categories);\n  if (count === 0) return Arr.map(series, () => []);\n  const span = view === undefined ? count : view.end - view.start;\n  const slot = box.width / Math.max(1, span);\n  const offset = view === undefined ? 0 : view.start;\n  const width =\n    series.length === 0 ? 0 : Math.max(1, (slot * 0.8) / series.length);\n  const zero = zeroY(box, range);\n  return Arr.map(series, (one, seriesIndex) =>\n    Arr.flatMap(\n      Arr.map(paintedValues(one.values, categories), (value, index) => ({\n        value,\n        x: (index + 0.1 - offset) * slot + seriesIndex * width,\n      })),\n      (cell: {\n        readonly value: number;\n        readonly x: number;\n      }): ReadonlyArray<ChartMark> => {\n        const y = scaleY(cell.value, box, range);\n        const height = Math.abs(y - zero);\n        if (!Number.isFinite(cell.value) || height < 0.5) return [];\n        return [\n          {\n            kind: \"rect\" as const,\n            x: cell.x,\n            y: Math.min(y, zero),\n            width,\n            height,\n            rx: Math.min(radius, width / 2, height / 2),\n          },\n        ];\n      },\n    ),\n  );\n};\n\nexport const horizontalBarMarks = (\n  series: ReadonlyArray<ChartSeries>,\n  box: ChartBox,\n  range: ChartRange,\n  categories?: number,\n  radius = 0,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const count = slotCount(series, categories);\n  if (count === 0) return Arr.map(series, () => []);\n  const slot = box.height / count;\n  const height =\n    series.length === 0 ? 0 : Math.max(1, (slot * 0.8) / series.length);\n  const zero = scaleX(0, box, range);\n  return Arr.map(series, (one, seriesIndex) =>\n    Arr.flatMap(\n      Arr.map(paintedValues(one.values, categories), (value, index) => ({\n        value,\n        y: index * slot + slot * 0.1 + seriesIndex * height,\n      })),\n      (cell: {\n        readonly value: number;\n        readonly y: number;\n      }): ReadonlyArray<ChartMark> => {\n        const x = scaleX(cell.value, box, range);\n        const barWidth = Math.abs(x - zero);\n        if (!Number.isFinite(cell.value) || barWidth < 0.5) return [];\n        return [\n          {\n            kind: \"rect\" as const,\n            x: Math.min(x, zero),\n            y: cell.y,\n            width: barWidth,\n            height,\n            rx: Math.min(radius, barWidth / 2, height / 2),\n          },\n        ];\n      },\n    ),\n  );\n};\n\nconst clampRadius = (reach: number | undefined, fallback: number): number =>\n  reach === undefined || !Number.isFinite(reach)\n    ? fallback\n    : Math.max(0, Math.min(fallback * 2 + 4, reach));\n\nconst radarPoint = (\n  value: number,\n  index: number,\n  axes: number,\n  box: ChartBox,\n  max: number,\n  outerRadius?: number,\n): Point => {\n  const cx = box.width / 2;\n  const cy = box.height / 2;\n  const radius = clampRadius(\n    outerRadius,\n    Math.max(0, Math.min(box.width, box.height) / 2 - 4),\n  );\n  const angle = -Math.PI / 2 + (index / Math.max(1, axes)) * Math.PI * 2;\n  const reach = max <= 0 ? 0 : (Math.max(0, finite(value)) / max) * radius;\n  return {\n    x: cx + reach * Math.cos(angle),\n    y: cy + reach * Math.sin(angle),\n  };\n};\n\nexport const radarPoints = (\n  values: ReadonlyArray<number>,\n  axes: number,\n  box: ChartBox,\n  max: number,\n  radius?: number,\n): ReadonlyArray<Point> =>\n  Arr.makeBy(axes, (index) =>\n    radarPoint(values[index] ?? 0, index, axes, box, max, radius),\n  );\n\nexport const polygonPath = (\n  points: ReadonlyArray<Point>,\n): Option.Option<string> =>\n  Option.map(Arr.head(points), (first) => {\n    return `M${pair(first)}${Arr.join(\n      Arr.map(Arr.drop(points, 1), (point) => `L${pair(point)}`),\n      \"\",\n    )}Z`;\n  });\n\nexport const radarGrid = (\n  levels: number,\n  axes: number,\n  box: ChartBox,\n  max = 1,\n  radius?: number,\n): ReadonlyArray<string> => {\n  const steps = Math.max(1, Math.min(8, Math.floor(levels)));\n  return Arr.flatMap(\n    Arr.makeBy(steps, (index) => index + 1),\n    (level) =>\n      Option.match(\n        polygonPath(\n          Arr.makeBy(axes, (axis) =>\n            radarPoint((max * level) / steps, axis, axes, box, max, radius),\n          ),\n        ),\n        {\n          onNone: () => [],\n          onSome: (d): ReadonlyArray<string> => [d],\n        },\n      ),\n  );\n};\n\nexport const radarGridAt = (\n  fractions: ReadonlyArray<number>,\n  axes: number,\n  box: ChartBox,\n  max = 1,\n  radius?: number,\n): ReadonlyArray<string> =>\n  Arr.flatMap(fractions, (fraction) => {\n    const share = Math.max(0, Math.min(1, fraction));\n    if (share <= 0) return [];\n    const reach =\n      clampRadius(\n        radius,\n        Math.max(0, Math.min(box.width, box.height) / 2 - 4),\n      ) * share;\n    return Option.match(\n      polygonPath(\n        Arr.makeBy(axes, (axis) =>\n          radarPoint(max, axis, axes, box, max, reach),\n        ),\n      ),\n      {\n        onNone: () => [],\n        onSome: (d): ReadonlyArray<string> => [d],\n      },\n    );\n  });\n\nexport const radarCircleGridAt = (\n  fractions: ReadonlyArray<number>,\n  box: ChartBox,\n  radius?: number,\n): ReadonlyArray<string> => {\n  const reach = clampRadius(\n    radius,\n    Math.max(0, Math.min(box.width, box.height) / 2 - 4),\n  );\n  return Arr.flatMap(fractions, (fraction) => {\n    const share = Math.max(0, Math.min(1, fraction));\n    return share <= 0\n      ? []\n      : [circlePath(box.width / 2, box.height / 2, reach * share)];\n  });\n};\n\nexport const radarSpokes = (\n  axes: number,\n  box: ChartBox,\n  max = 1,\n  radius?: number,\n): ReadonlyArray<string> =>\n  Arr.makeBy(axes, (axis) => {\n    const rim = radarPoint(max, axis, axes, box, max, radius);\n    return `M${pair({ x: box.width / 2, y: box.height / 2 })}L${pair(rim)}`;\n  });\n\nexport const circlePath = (cx: number, cy: number, radius: number): string =>\n  `M${at(cx - radius)} ${at(cy)}A${at(radius)} ${at(radius)} 0 1 0 ${at(cx + radius)} ${at(cy)}A${at(radius)} ${at(radius)} 0 1 0 ${at(cx - radius)} ${at(cy)}Z`;\n\nexport const radarCircleGrid = (\n  levels: number,\n  box: ChartBox,\n  reach?: number,\n): ReadonlyArray<string> => {\n  const steps = Math.max(1, Math.min(8, Math.floor(levels)));\n  const radius = clampRadius(\n    reach,\n    Math.max(0, Math.min(box.width, box.height) / 2 - 4),\n  );\n  return Arr.makeBy(steps, (index) =>\n    circlePath(box.width / 2, box.height / 2, (radius * (index + 1)) / steps),\n  );\n};\n\nexport const fullRing = (\n  cx: number,\n  cy: number,\n  outer: number,\n  inner: number,\n): string =>\n  `${ringArc(cx, cy, outer, inner, -Math.PI / 2, 0)}${ringArc(cx, cy, outer, inner, 0, (Math.PI * 3) / 2)}`;\n\nexport const ringArc = (\n  cx: number,\n  cy: number,\n  outer: number,\n  inner: number,\n  start: number,\n  end: number,\n): string => {\n  const point = (radius: number, angle: number): string =>\n    `${at(cx + radius * Math.cos(angle))} ${at(cy + radius * Math.sin(angle))}`;\n  const span = Math.abs(end - start);\n  const large = span > Math.PI ? 1 : 0;\n  return [\n    `M${point(outer, start)}`,\n    `A${at(outer)} ${at(outer)} 0 ${large} 1 ${point(outer, end)}`,\n    `L${point(inner, end)}`,\n    `A${at(inner)} ${at(inner)} 0 ${large} 0 ${point(inner, start)}`,\n    \"Z\",\n  ].join(\"\");\n};\n\nexport const radialArcs = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  max: number,\n  thickness = 10,\n  gap = 4,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const cx = box.width / 2;\n  const cy = box.height / 2;\n  const outer = Math.max(0, Math.min(box.width, box.height) / 2 - 1);\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n  return Arr.map(values, (value, index) => {\n    const ringOuter = outer - index * (thickness + gap);\n    const ringInner = ringOuter - thickness;\n    if (ringOuter <= 0 || ringInner < 0) return [];\n    const share = max <= 0 ? 0 : Math.max(0, finite(value)) / max;\n    const span = share * full;\n    if (span <= 0) return [];\n    if (span >= full - 1e-9) {\n      return [\n        {\n          kind: \"path\" as const,\n          d: ringArc(cx, cy, ringOuter, ringInner, start, start + Math.PI),\n        },\n        {\n          kind: \"path\" as const,\n          d: ringArc(\n            cx,\n            cy,\n            ringOuter,\n            ringInner,\n            start + Math.PI,\n            start + full,\n          ),\n        },\n      ];\n    }\n    return [\n      {\n        kind: \"path\" as const,\n        d: ringArc(cx, cy, ringOuter, ringInner, start, start + span),\n      },\n    ];\n  });\n};\n\nexport const ringSegments = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  innerRatio = 0,\n  padAngle = 0,\n  emphasis?: number,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const drawable = Arr.map(values, (value) => Math.max(0, finite(value)));\n  const total = Arr.reduce(drawable, 0, (sum, value) => sum + value);\n  if (total <= 0) return Arr.map(values, () => []);\n  const size = Math.min(box.width, box.height);\n  const cx = box.width / 2;\n  const cy = box.height / 2;\n  const outer = size / 2;\n  const inner = outer * Math.min(0.95, Math.max(0, innerRatio));\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n  const gap = Math.min(\n    Math.max(0, padAngle),\n    (full * 0.4) / Math.max(1, drawable.length),\n  );\n  const [, segments] = Arr.mapAccum(drawable, start, (angle, value) => {\n    if (value <= 0) return [angle, []];\n    const span = (value / total) * full;\n    const from = angle + gap / 2;\n    const to = angle + span - gap / 2;\n    if (to <= from) return [angle + span, []];\n    const index = Math.max(\n      0,\n      Math.round(((angle - start) / Math.max(1e-9, full)) * drawable.length),\n    );\n    const rim = index === emphasis ? outer + 6 : outer;\n    if (span >= full - 1e-9) {\n      return [\n        angle + full,\n        [\n          {\n            kind: \"path\" as const,\n            d: ringArc(cx, cy, rim, inner, from, from + Math.PI),\n          },\n          {\n            kind: \"path\" as const,\n            d: ringArc(cx, cy, rim, inner, from + Math.PI, from + full),\n          },\n        ],\n      ];\n    }\n    return [\n      angle + span,\n      [{ kind: \"path\" as const, d: ringArc(cx, cy, rim, inner, from, to) }],\n    ];\n  });\n  return segments;\n};\n\nexport const ringLabelPoints = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  innerRatio = 0,\n): ReadonlyArray<Option.Option<Point>> => {\n  const drawable = Arr.map(values, (value) => Math.max(0, finite(value)));\n  const total = Arr.reduce(drawable, 0, (sum, value) => sum + value);\n  const size = Math.min(box.width, box.height);\n  const outer = size / 2;\n  const reach = (outer + outer * innerRatio) / 2;\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n  const [, points] = Arr.mapAccum(drawable, start, (angle, value) => {\n    if (total <= 0 || value <= 0) return [angle, Option.none()];\n    const span = (value / total) * full;\n    const middle = angle + span / 2;\n    return [\n      angle + span,\n      Option.some({\n        x: box.width / 2 + reach * Math.cos(middle),\n        y: box.height / 2 + reach * Math.sin(middle),\n      }),\n    ];\n  });\n  return points;\n};\n\nexport interface RingLabel {\n  readonly point: Point;\n  readonly right: boolean;\n  readonly leader: string;\n}\n\nexport const ringOutsideLabels = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n): ReadonlyArray<Option.Option<RingLabel>> => {\n  const drawable = Arr.map(values, (value) => Math.max(0, finite(value)));\n  const total = Arr.reduce(drawable, 0, (sum, value) => sum + value);\n  const size = Math.min(box.width, box.height);\n  const outer = size / 2;\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n  const step = 14;\n  const [, labels] = Arr.mapAccum(drawable, start, (angle, value) => {\n    if (total <= 0 || value <= 0) return [angle, Option.none()];\n    const span = (value / total) * full;\n    const middle = angle + span / 2;\n    const right = Math.cos(middle) >= 0;\n    const rimX = box.width / 2 + (outer - 2) * Math.cos(middle);\n    const rimY = box.height / 2 + (outer - 2) * Math.sin(middle);\n    const elbowX = box.width / 2 + (outer + step) * Math.cos(middle);\n    const elbowY = box.height / 2 + (outer + step) * Math.sin(middle);\n    const anchorX = elbowX + (right ? step : -step);\n    return [\n      angle + span,\n      Option.some({\n        point: { x: anchorX, y: elbowY },\n        right,\n        leader: `M${at(rimX)} ${at(rimY)}L${at(elbowX)} ${at(elbowY)}L${at(anchorX)} ${at(elbowY)}`,\n      } satisfies RingLabel),\n    ];\n  });\n  return labels;\n};\n\nexport interface ChartView {\n  readonly start: number;\n  readonly end: number;\n}\n\nexport const chartView = (\n  length: number,\n  view: readonly [number, number] | undefined,\n): Option.Option<ChartView> => {\n  if (view === undefined || length <= 0) return Option.none();\n  const [left, right] = view;\n  if (!Number.isFinite(left) || !Number.isFinite(right)) return Option.none();\n  const low = Math.min(left, right);\n  const high = Math.max(left, right);\n  const end = Math.min(length, Math.max(low + 1, high));\n  const start = Math.max(0, Math.min(end - 1, low));\n  return Option.some({ start, end });\n};\n\nexport interface ChartWindow {\n  readonly start: number;\n  readonly end: number;\n}\n\nexport const chartWindow = (\n  length: number,\n  domain: readonly [number, number] | undefined,\n): Option.Option<ChartWindow> => {\n  if (domain === undefined || length <= 0) return Option.none();\n  const [left, right] = domain;\n  if (!Number.isFinite(left) || !Number.isFinite(right)) return Option.none();\n  const edgeLow = Math.min(left, right);\n  const edgeHigh = Math.max(left, right);\n  if (edgeHigh <= 0 || edgeLow >= length) return Option.none();\n  const low = Math.max(\n    0,\n    Math.min(length - 1, Math.floor(Math.min(left, right))),\n  );\n  const high = Math.max(\n    low + 1,\n    Math.min(length, Math.ceil(Math.max(left, right))),\n  );\n  if (high <= 0 || low >= length) return Option.none();\n  return Option.some({ start: low, end: high });\n};\n\nexport const ringBands = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  innerRatio = 0,\n  padAngle = 0,\n  emphasis?: number,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const drawable = Arr.map(values, (value) => Math.max(0, finite(value)));\n  const total = Arr.reduce(drawable, 0, (sum, value) => sum + value);\n  if (total <= 0) return Arr.map(values, () => []);\n  const size = Math.min(box.width, box.height);\n  const cx = box.width / 2;\n  const cy = box.height / 2;\n  const outer = size / 2;\n  const inner = outer * Math.min(0.95, Math.max(0, innerRatio));\n  const bands = Math.max(1, drawable.length);\n  const band = (outer - inner) / bands;\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n  const gap = Math.min(\n    Math.max(0, padAngle),\n    (full * 0.4) / Math.max(1, drawable.length),\n  );\n  return Arr.map(drawable, (value, index) => {\n    if (value <= 0) return [];\n    const rim = outer - index * band + (index === emphasis ? 4 : 0);\n    const floor = Math.max(0, rim - band);\n    const span = (value / total) * full;\n    const from = start + gap / 2;\n    const to = start + span - gap / 2;\n    if (to <= from) return [];\n    if (span >= full - 1e-9) {\n      return [\n        {\n          kind: \"path\" as const,\n          d: ringArc(cx, cy, rim, floor, from, from + Math.PI),\n        },\n        {\n          kind: \"path\" as const,\n          d: ringArc(cx, cy, rim, floor, from + Math.PI, from + full),\n        },\n      ];\n    }\n    return [\n      { kind: \"path\" as const, d: ringArc(cx, cy, rim, floor, from, to) },\n    ];\n  });\n};\n\nexport const valueLabel = (value: number): string =>\n  Number.isFinite(value) ? `${Math.round(value * 10) / 10}` : \"0\";\n\nexport const rangeTicks = (\n  range: ChartRange,\n  count = 4,\n): ReadonlyArray<number> => {\n  const steps = Number.isFinite(count)\n    ? Math.min(20, Math.max(1, Math.floor(count)))\n    : 4;\n  const span = range.max - range.min;\n  return Arr.makeBy(steps + 1, (index) => range.min + (span / steps) * index);\n};\n\nexport const pinnedRange = (\n  range: readonly [number, number] | undefined,\n): Option.Option<ChartRange> => {\n  if (range === undefined) return Option.none();\n  const [low, high] = range;\n  if (!Number.isFinite(low) || !Number.isFinite(high)) return Option.none();\n  const min = Math.min(low, high);\n  const max = Math.max(low, high);\n  if (max - min <= 0) return Option.none();\n  return Option.some({ min, max });\n};\n\nexport const chartRangeWith = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n  pinned: readonly [number, number] | undefined = undefined,\n  stacked = false,\n): ChartRange =>\n  Option.getOrElse(pinnedRange(pinned), () =>\n    chartRange(series, categories, stacked),\n  );\n\nexport const referenceLines = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  range: ChartRange,\n): ReadonlyArray<{ readonly y: number; readonly value: number }> =>\n  Arr.map(Arr.filter(values, Number.isFinite), (value) => ({\n    y: scaleY(value, box, range),\n    value,\n  }));\n\nexport const verticalGrid = (\n  count: number,\n  box: ChartBox,\n): ReadonlyArray<{ readonly x: number; readonly width: number }> => {\n  const steps = Number.isFinite(count)\n    ? Math.min(64, Math.max(0, Math.floor(count)))\n    : 0;\n  if (steps === 0) return [];\n  const slot = box.width / steps;\n  return Arr.map(\n    Arr.makeBy(steps, (index) => index),\n    (index) => ({\n      x: index * slot,\n      width: Math.max(1, slot),\n    }),\n  );\n};\n\nexport const scatterMarks = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  range: ChartRange,\n  categories?: number,\n  radius = 3,\n): ReadonlyArray<ChartMark> => {\n  const count = slotCount([], categories);\n  if (count === 0) return [];\n  const slot = box.width / count;\n  return Arr.map(\n    Arr.filter(\n      Arr.map(paintedValues(values, categories), (value, index) => ({\n        value,\n        x: (index + 0.5) * slot,\n      })),\n      (cell) => Number.isFinite(cell.value),\n    ),\n    (cell): ChartMark => ({\n      kind: \"circle\",\n      cx: cell.x,\n      cy: Math.max(0, Math.min(box.height, scaleY(cell.value, box, range))),\n      r: Math.max(0.01, radius),\n    }),\n  );\n};\n\nexport interface Candle {\n  readonly open: number;\n  readonly high: number;\n  /** The lowest price the period reached. */\n  readonly low: number;\n  /** The price the period closed at. */\n  readonly close: number;\n}\n\n/** A candle's direction: an up day or a down day. */\nexport const candleUp = (candle: Candle): boolean => candle.close > candle.open;\n\nexport const candleMarks = (\n  candle: Candle,\n  x: number,\n  width: number,\n  box: ChartBox,\n  range: ChartRange,\n): ReadonlyArray<ChartMark> => {\n  const numbers = [candle.open, candle.high, candle.low, candle.close];\n  if (Arr.some(numbers, (value) => !Number.isFinite(value))) return [];\n  const high = scaleY(candle.high, box, range);\n  const low = scaleY(candle.low, box, range);\n  const open = scaleY(candle.open, box, range);\n  const close = scaleY(candle.close, box, range);\n  const top = Math.min(open, close);\n  const bodyHeight = Math.max(1, Math.abs(close - open));\n  return [\n    {\n      kind: \"rect\",\n      x: x + width / 2 - 0.5,\n      y: Math.min(high, low),\n      width: 1,\n      height: Math.abs(low - high),\n    },\n    {\n      kind: \"rect\",\n      x,\n      y: top,\n      width,\n      height: bodyHeight,\n    },\n  ];\n};\n\nexport const candleAtMarks = (\n  candle: Candle,\n  index: number,\n  categories: number,\n  box: ChartBox,\n  range: ChartRange,\n  width = 6,\n): ReadonlyArray<ChartMark> => {\n  const count = slotCount([], categories);\n  if (count === 0) return [];\n  const slot = box.width / count;\n  const bodyWidth = Math.max(1, Math.min(width, slot * 0.6));\n  return candleMarks(\n    candle,\n    (index + 0.5) * slot - bodyWidth / 2,\n    bodyWidth,\n    box,\n    range,\n  );\n};\n\nexport const candleSeriesMarks = (\n  candles: ReadonlyArray<Candle>,\n  box: ChartBox,\n  range: ChartRange,\n  categories?: number,\n  width = 6,\n): ReadonlyArray<ChartMark> => {\n  const count = slotCount([], categories);\n  if (count === 0) return [];\n  const painted = Arr.take(candles, count);\n  const slot = box.width / count;\n  const bodyWidth = Math.max(1, Math.min(width, slot * 0.6));\n  return Arr.flatMap(painted, (candle, index) =>\n    candleMarks(\n      candle,\n      (index + 0.5) * slot - bodyWidth / 2,\n      bodyWidth,\n      box,\n      range,\n    ),\n  );\n};\n\nexport const funnelMarks = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n): ReadonlyArray<ChartMark> => {\n  const drawable = Arr.map(values, (value) =>\n    Number.isFinite(value) && value > 0 ? value : 0,\n  );\n  const first = Arr.head(drawable);\n  if (Option.isNone(first) || first.value <= 0) return [];\n  const widest = first.value;\n  const count = drawable.length;\n  const slot = box.height / count;\n  return Arr.map(\n    Arr.filter(\n      Arr.map(drawable, (value, index) => ({ value, index })),\n      (cell) => cell.value > 0,\n    ),\n    (cell): ChartMark => ({\n      kind: \"rect\",\n      x: (box.width - (cell.value / widest) * box.width) / 2,\n      y: cell.index * slot,\n      width: (cell.value / widest) * box.width,\n      height: Math.max(1, slot * 0.8),\n    }),\n  );\n};\n"
    },
    {
      "path": "packages/data/src/chart/hierarchy.ts",
      "target": "@ui/gpuixui/data/chart/hierarchy.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\n\nexport const HIERARCHY_SEPARATOR = \"/\";\n\nexport interface HierarchyNode {\n  readonly path: string;\n\n  readonly value?: number;\n}\n\nexport interface HierarchyRow {\n  readonly path: string;\n\n  readonly name: string;\n\n  readonly value: number;\n\n  readonly key: string;\n}\n\nconst ROOT_PATH = \"\\u0000root\";\n\nexport const hierarchyRows = (\n  nodes: ReadonlyArray<HierarchyNode>,\n): ReadonlyArray<HierarchyRow> => {\n  if (nodes.length === 0) return [];\n\n  const paths = nodes.map((node) => node.path);\n  const present = new Set(paths);\n\n  const parentOf = (path: string): string | undefined => {\n    const at = path.lastIndexOf(HIERARCHY_SEPARATOR);\n    if (at <= 0) return undefined;\n    const parent = path.slice(0, at);\n    return present.has(parent) ? parent : undefined;\n  };\n\n  const rootPaths = paths.filter((path) => parentOf(path) === undefined);\n  const roots =\n    rootPaths.length > 1 && shareAParent(rootPaths)\n      ? [rootPaths[0] ?? \"\"]\n      : rootPaths;\n\n  const sums = new Map<string, number>();\n  for (const node of nodes) {\n    const parent = parentOf(node.path);\n    if (parent === undefined) continue;\n    const value =\n      node.value === undefined || !Number.isFinite(node.value) ? 0 : node.value;\n    sums.set(parent, (sums.get(parent) ?? 0) + value);\n  }\n\n  const rows: HierarchyRow[] = nodes.map((node) => ({\n    path: node.path,\n    name: nameOf(node.path),\n\n    value: finiteOr(node.value, sums.get(node.path) ?? 0),\n    key: node.path,\n  }));\n\n  if (roots.length === 1) return rows;\n\n  const rootValue = rows.reduce((total, row) => total + row.value, 0);\n  return [\n    { path: ROOT_PATH, name: ROOT_PATH, value: rootValue, key: ROOT_PATH },\n    ...rows.map((row) => ({\n      ...row,\n      path: `${ROOT_PATH}/${row.path}`,\n    })),\n  ];\n};\n\nconst finiteOr = (value: number | undefined, fallback: number): number =>\n  value === undefined || !Number.isFinite(value) ? fallback : value;\n\nconst shareAParent = (paths: ReadonlyArray<string>): boolean => {\n  const first = paths[0];\n  if (first === undefined) return false;\n  const at = first.lastIndexOf(HIERARCHY_SEPARATOR);\n  if (at <= 0) return false;\n  const parent = first.slice(0, at);\n  return Arr.every(paths, (path) => {\n    const cut = path.lastIndexOf(HIERARCHY_SEPARATOR);\n    return cut > 0 && path.slice(0, cut) === parent;\n  });\n};\n\nexport const hierarchyRoots = (\n  rows: ReadonlyArray<HierarchyRow>,\n): ReadonlyArray<HierarchyRow> => {\n  const paths = rows.map((row) => row.path);\n  return rows.filter(\n    (row) =>\n      !paths.some(\n        (path) => path !== row.path && row.path.startsWith(`${path}/`),\n      ),\n  );\n};\n\nexport const nameOf = (path: string): string => {\n  const at = path.lastIndexOf(HIERARCHY_SEPARATOR);\n  return at === -1 ? path : path.slice(at + 1);\n};\n\nexport const hierarchyLeaves = (\n  rows: ReadonlyArray<HierarchyRow>,\n): ReadonlyArray<HierarchyRow> => {\n  const parents = new Set(\n    rows\n      .map((row) => parentPath(row.path))\n      .filter((path) => path !== undefined),\n  );\n  return rows.filter((row) => !parents.has(row.path));\n};\n\nconst parentPath = (path: string): string | undefined => {\n  const at = path.lastIndexOf(HIERARCHY_SEPARATOR);\n  return at <= 0 ? undefined : path.slice(0, at);\n};\n\nexport interface FlowNode {\n  readonly id: string;\n\n  readonly name?: string;\n}\n\nexport interface FlowLink {\n  readonly source: string;\n\n  readonly target: string;\n\n  readonly value: number;\n}\n\nexport const flowGraph = (\n  nodes: ReadonlyArray<FlowNode>,\n  links: ReadonlyArray<FlowLink>,\n): {\n  readonly nodes: ReadonlyArray<FlowNode>;\n  readonly links: ReadonlyArray<FlowLink>;\n} => {\n  const present = new Set(nodes.map((node) => node.id));\n  return {\n    nodes,\n    links: links.filter(\n      (link) =>\n        present.has(link.source) &&\n        present.has(link.target) &&\n        Number.isFinite(link.value) &&\n        link.value > 0,\n    ),\n  };\n};\n"
    },
    {
      "path": "packages/data/src/chart/legend.tsx",
      "target": "@ui/gpuixui/data/chart/legend.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport type { Props } from \"@gpuix/react\";\nimport { iconStepValue } from \"../../primitives/core/index.js\";\nimport * as Arr from \"effect/Array\";\nimport { recordsOf } from \"../internal/list.js\";\nimport { textStyle, themeColors, useTheme } from \"../theme.js\";\nimport { CHART_LEGEND, CHART_LEGEND_ITEM, CHART_SWATCH } from \"./recipe.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { seriesName } from \"./logic.js\";\nimport type { LegendShape } from \"./frame.js\";\n\nexport interface ChartLegendItem {\n  readonly key: string;\n\n  readonly label: string;\n\n  readonly color: string;\n\n  readonly value?: string;\n}\n\nexport type { LegendShape, TooltipIndicator } from \"./frame.js\";\n\nexport interface ChartLegendProps extends Omit<Props, \"children\"> {\n  readonly items: ReadonlyArray<ChartLegendItem>;\n\n  readonly shape?: LegendShape;\n  readonly testId?: string;\n}\n\nexport const ChartLegend = ({\n  items,\n  shape = \"square\",\n  testId,\n  style,\n  ...rest\n}: ChartLegendProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const rows = recordsOf(items);\n  return (\n    <div\n      {...rest}\n      testId={testId}\n      style={mergeStyles(recipeStyle(CHART_LEGEND, theme), style)}\n    >\n      {Arr.map(rows, (item) => (\n        <div key={item.key} style={recipeStyle(CHART_LEGEND_ITEM, theme)}>\n          <div\n            testId={\n              testId === undefined ? undefined : `${testId}-swatch-${item.key}`\n            }\n            style={{\n              ...recipeStyle(CHART_SWATCH, theme),\n              width:\n                shape === \"line\"\n                  ? iconStepValue(theme, \"xs\") * 2\n                  : iconStepValue(theme, \"xs\"),\n              height: shape === \"line\" ? 2 : iconStepValue(theme, \"xs\"),\n              borderRadius:\n                shape === \"dot\"\n                  ? iconStepValue(theme, \"xs\") / 2\n                  : shape === \"line\"\n                    ? 1\n                    : undefined,\n              backgroundColor: item.color,\n            }}\n          />\n          <text\n            style={{\n              ...textStyle(theme, \"caption\"),\n              color: colors.mutedForeground,\n            }}\n          >\n            {item.value === undefined\n              ? seriesName(item.label, item.key)\n              : `${seriesName(item.label, item.key)} ${item.value}`}\n          </text>\n        </div>\n      ))}\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/chart/logic.ts",
      "target": "@ui/gpuixui/data/chart/logic.ts",
      "type": "registry:lib",
      "content": "import { spokenText } from \"../../primitives/core/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport { flatMark, marksMarkup, num, type ChartMark } from \"./mark.js\";\n\nexport { type ChartMark } from \"./mark.js\";\n\nexport interface ChartSeries {\n  readonly key: string;\n  readonly label: string;\n  readonly values: ReadonlyArray<number>;\n  readonly color?: string;\n}\n\nexport interface ChartBox {\n  readonly width: number;\n  readonly height: number;\n}\n\nconst paintableSide = (side: number, fallback: number): number =>\n  Number.isFinite(side) && side > 0\n    ? side\n    : Math.max(1, Number.isFinite(fallback) && fallback > 0 ? fallback : 1);\n\nexport const chartBox = (\n  width: number,\n  height: number,\n  fallback: ChartBox,\n): ChartBox => ({\n  width: paintableSide(width, fallback.width),\n  height: paintableSide(height, fallback.height),\n});\n\nexport const drawableValue = (value: number): number =>\n  Number.isFinite(value) && value > 0 ? value : 0;\n\nconst paintedSlots = (categories?: number): Option.Option<number> =>\n  Option.map(\n    Option.filter(Option.fromNullishOr(categories), Number.isFinite),\n    (count) => Math.max(0, Math.floor(count)),\n  );\n\nconst slotIndexes = (count: number): ReadonlyArray<number> =>\n  count <= 0 ? [] : Arr.makeBy(count, (index) => index);\n\nconst paintedValues = (\n  values: ReadonlyArray<number>,\n  categories?: number,\n): ReadonlyArray<number> =>\n  Option.match(paintedSlots(categories), {\n    onNone: () => values,\n    onSome: (count) => Arr.take(values, count),\n  });\n\nexport const seriesMax = (series: ChartSeries, categories?: number): number =>\n  Arr.reduce(paintedValues(series.values, categories), 0, (max, value) =>\n    Math.max(max, drawableValue(value)),\n  );\n\nexport const chartMaxOf = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n): number =>\n  Arr.reduce(series, 0, (max, one) =>\n    Math.max(max, seriesMax(one, categories)),\n  );\n\nexport const niceCeiling = (raw: number): number => {\n  const value = drawableValue(raw);\n  if (value === 0) return 1;\n  const magnitude = 10 ** Math.floor(Math.log10(value));\n  const step = value / magnitude;\n  const nice = step <= 1 ? 1 : step <= 2 ? 2 : step <= 5 ? 5 : 10;\n  return nice * magnitude;\n};\n\nexport const chartScale = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n): number => niceCeiling(chartMaxOf(series, categories));\n\nconst categoryCount = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n): number =>\n  Option.getOrElse(paintedSlots(categories), () =>\n    Arr.reduce(series, 0, (most, one) => Math.max(most, one.values.length)),\n  );\n\nexport const stackedScale = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n): number =>\n  niceCeiling(\n    Arr.reduce(\n      Arr.map(slotIndexes(categoryCount(series, categories)), (index) =>\n        Arr.reduce(\n          series,\n          0,\n          (sum, one) => sum + drawableValue(one.values[index] ?? 0),\n        ),\n      ),\n      0,\n      Math.max,\n    ),\n  );\n\nexport const AXIS_STEP_LIMIT = 20;\n\nconst axisSteps = (count: number): number =>\n  Number.isFinite(count)\n    ? Math.min(AXIS_STEP_LIMIT, Math.max(1, Math.floor(count)))\n    : 4;\n\nexport const axisTicks = (scale: number, count = 4): ReadonlyArray<number> => {\n  const steps = axisSteps(count);\n  return Arr.makeBy(steps + 1, (index) => (scale / steps) * index);\n};\n\nexport const tickLabel = (tick: number, step: number): string => {\n  if (!Number.isFinite(tick)) return \"0\";\n  if (!Number.isFinite(step) || step <= 0) return `${tick}`;\n  const decimals = Math.max(0, Math.ceil(-Math.log10(step)) + 1);\n  if (decimals > 20) return tick === 0 ? \"0\" : tick.toExponential(2);\n  return `${Number(tick.toFixed(decimals))}`;\n};\n\nexport interface BarSlot {\n  readonly slot: number;\n  readonly barWidth: number;\n}\n\nconst slotCount = (count: number): number => Math.max(1, Math.floor(count));\n\nexport const barSlot = (count: number, width: number, share = 0.6): BarSlot => {\n  const slot = Math.max(0, width) / slotCount(count);\n  return { slot, barWidth: slot * Math.min(1, Math.max(0, share)) };\n};\n\nconst barHeight = (value: number, box: ChartBox, scale: number): number => {\n  if (scale <= 0) return 0;\n  const raw = (drawableValue(value) / scale) * Math.max(0, box.height);\n  return Math.min(Math.max(0, box.height), raw);\n};\n\nexport const barMarks = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  scale: number,\n  categories: number = values.length,\n): ReadonlyArray<ChartMark> => {\n  const { slot, barWidth } = barSlot(categories, box.width);\n  return Arr.map(\n    Arr.filter(\n      Arr.map(paintedValues(values, categories), (value, index) => ({\n        index,\n        height: barHeight(value, box, scale),\n      })),\n      (one) => one.height > 0,\n    ),\n    (one): ChartMark => ({\n      kind: \"rect\",\n      x: one.index * slot + (slot - barWidth) / 2,\n      y: box.height - one.height,\n      width: barWidth,\n      height: one.height,\n    }),\n  );\n};\n\nexport const groupedBarMarks = (\n  series: ReadonlyArray<ChartSeries>,\n  box: ChartBox,\n  scale: number,\n  categories?: number,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const count = categoryCount(series, categories);\n  const { slot } = barSlot(count, box.width);\n  const width = series.length === 0 ? 0 : (slot * 0.8) / series.length;\n  return Arr.map(series, (one, seriesIndex) =>\n    Arr.map(\n      Arr.filter(\n        Arr.map(paintedValues(one.values, count), (value, index) => ({\n          index,\n          height: barHeight(value, box, scale),\n        })),\n        (candidate) => candidate.height > 0,\n      ),\n      (candidate): ChartMark => ({\n        kind: \"rect\",\n        x: candidate.index * slot + slot * 0.1 + seriesIndex * width,\n        y: box.height - candidate.height,\n        width,\n        height: candidate.height,\n      }),\n    ),\n  );\n};\n\nexport const stackedBarMarks = (\n  series: ReadonlyArray<ChartSeries>,\n  box: ChartBox,\n  scale: number,\n  categories?: number,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const count = categoryCount(series, categories);\n  const totals = Arr.map(slotIndexes(count), (index) =>\n    Arr.reduce(\n      series,\n      0,\n      (sum, one) => sum + drawableValue(one.values[index] ?? 0),\n    ),\n  );\n  const { slot, barWidth } = barSlot(count, box.width, 0.7);\n  return Arr.map(series, (_, seriesIndex) =>\n    Arr.map(\n      Arr.filter(\n        Arr.map(totals, (_, index) => {\n          const below = Arr.reduce(\n            Arr.take(series, seriesIndex),\n            0,\n            (sum, one) => sum + drawableValue(one.values[index] ?? 0),\n          );\n          const value = drawableValue(series[seriesIndex]?.values[index] ?? 0);\n          const height = barHeight(value, box, scale);\n          return {\n            index,\n            height,\n            base: barHeight(below + value, box, scale),\n          };\n        }),\n        (candidate) => candidate.height > 0,\n      ),\n      (candidate): ChartMark => ({\n        kind: \"rect\",\n        x: candidate.index * slot + (slot - barWidth) / 2,\n        y: box.height - candidate.base,\n        width: barWidth,\n        height: candidate.height,\n      }),\n    ),\n  );\n};\n\nexport const linePath = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  scale: number,\n  categories: number = values.length,\n): Option.Option<string> => {\n  const painted = paintedValues(values, categories);\n  if (painted.length === 0) return Option.none();\n  const { slot } = barSlot(\n    Option.getOrElse(paintedSlots(categories), () => values.length),\n    box.width,\n  );\n  const points = Arr.map(painted, (value, index) => ({\n    x: index * slot + slot / 2,\n    y: box.height - barHeight(value, box, scale),\n  }));\n  const path = Arr.join(\n    Arr.map(\n      points,\n      (point, index) =>\n        `${index === 0 ? \"M\" : \"L\"}${num(point.x)} ${num(point.y)}`,\n    ),\n    \"\",\n  );\n  return Option.some(painted.length === 1 ? `${path}h1` : path);\n};\n\nconst arcPath = (\n  cx: number,\n  cy: number,\n  outer: number,\n  inner: number,\n  start: number,\n  end: number,\n): string => {\n  const point = (radius: number, angle: number): string =>\n    `${num(cx + radius * Math.cos(angle))} ${num(cy + radius * Math.sin(angle))}`;\n  const span = end - start;\n  const large = span > Math.PI ? 1 : 0;\n  return [\n    `M${point(outer, start)}`,\n    `A${num(outer)} ${num(outer)} 0 ${large} 1 ${point(outer, end)}`,\n    `L${point(inner, end)}`,\n    `A${num(inner)} ${num(inner)} 0 ${large} 0 ${point(inner, start)}`,\n    \"Z\",\n  ].join(\"\");\n};\n\nexport const donutSegments = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const drawable = Arr.map(values, drawableValue);\n  const total = Arr.reduce(drawable, 0, (sum, value) => sum + value);\n  if (total <= 0) return Arr.map(values, () => []);\n  const size = Math.min(box.width, box.height);\n  const cx = box.width / 2;\n  const cy = box.height / 2;\n  const outer = size / 2;\n  const inner = outer * 0.6;\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n\n  const [, segments] = Arr.mapAccum(\n    drawable,\n    start,\n    (angle, value): readonly [number, ReadonlyArray<ChartMark>] => {\n      if (value <= 0) return [angle, []];\n      const span = (value / total) * full;\n      if (span >= full - 1e-9) {\n        return [\n          angle + full,\n          [\n            {\n              kind: \"path\",\n              d: arcPath(cx, cy, outer, inner, angle, angle + Math.PI),\n            },\n            {\n              kind: \"path\",\n              d: arcPath(cx, cy, outer, inner, angle + Math.PI, angle + full),\n            },\n          ],\n        ];\n      }\n      return [\n        angle + span,\n        [\n          {\n            kind: \"path\",\n            d: arcPath(cx, cy, outer, inner, angle, angle + span),\n          },\n        ],\n      ];\n    },\n  );\n  return segments;\n};\n\n/** Every ring segment, flattened — a donut chart's marks in one list. */\nexport const donutMarks = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n): ReadonlyArray<ChartMark> => Arr.flatten(donutSegments(values, box));\n\nexport const seriesName = (label: string, key: string): string =>\n  spokenText(Option.fromNullishOr(label), key);\n\nexport const seriesDescription = (\n  values: ReadonlyArray<number>,\n  labels: ReadonlyArray<string> = [],\n): string => {\n  const count = labels.length === 0 ? values.length : labels.length;\n  return Arr.join(\n    Arr.map(Arr.take(values, count), (value, index) => {\n      const readout = Number.isFinite(value)\n        ? `${Math.round(value * 100) / 100}`\n        : \"no value\";\n      const label = labels[index];\n      // an absent label already follows.\n      return P.isString(label) && label.trim().length > 0\n        ? `${label} ${readout}`\n        : readout;\n    }),\n    \", \",\n  );\n};\n\nexport const chartMarkup = (\n  marks: ReadonlyArray<ChartMark>,\n  box: ChartBox,\n  clip?: ChartMark[\"clip\"],\n): string => {\n  const flat = Arr.map(marks, (mark) => {\n    const shape = flatMark(mark);\n    return {\n      ...shape,\n      ...(clip === undefined ? {} : { clip }),\n      fill: shape.fill === undefined ? { color: \"#000\" } : shape.fill,\n    };\n  });\n  const painted = marksMarkup(flat);\n  const defs = painted.defs.length === 0 ? \"\" : `<defs>${painted.defs}</defs>`;\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${num(box.width)}\" height=\"${num(box.height)}\" viewBox=\"0 0 ${num(box.width)} ${num(box.height)}\">${defs}${painted.body}</svg>`;\n};\n\n/** The colour a series paints: the categorical scale, cycling. */\nexport const seriesColour = (\n  palette: ReadonlyArray<string>,\n  index: number,\n): string => {\n  if (palette.length === 0) return \"#000000\";\n  const at =\n    ((Math.floor(index) % palette.length) + palette.length) % palette.length;\n  return palette[at] ?? \"#000000\";\n};\n"
    },
    {
      "path": "packages/data/src/chart/mark.ts",
      "target": "@ui/gpuixui/data/chart/mark.ts",
      "type": "registry:lib",
      "content": "import { Match } from \"effect\";\nimport * as Arr from \"effect/Array\";\n\nexport interface MarkFill {\n  readonly color: string;\n  readonly opacity?: number;\n  readonly gradient?: string;\n}\n\nexport interface MarkStroke {\n  readonly color: string;\n  readonly width: number;\n  readonly opacity?: number;\n  readonly dash?: string;\n}\n\nexport interface MarkClip {\n  readonly x: number;\n  readonly y: number;\n  readonly width: number;\n  readonly height: number;\n}\n\nexport interface MarkPaint {\n  readonly fill?: MarkFill;\n  readonly stroke?: MarkStroke;\n  readonly clip?: MarkClip;\n}\n\nexport interface RectMark extends MarkPaint {\n  readonly kind: \"rect\";\n  readonly x: number;\n  readonly y: number;\n  readonly width: number;\n  readonly height: number;\n  readonly rx?: number;\n}\n\nexport interface PathMark extends MarkPaint {\n  readonly kind: \"path\";\n  readonly d: string;\n}\n\n/** A circle, with its paint. */\nexport interface CircleMark extends MarkPaint {\n  readonly kind: \"circle\";\n  readonly cx: number;\n  readonly cy: number;\n  readonly r: number;\n}\n\n/** An ellipse, with its paint. */\nexport interface EllipseMark extends MarkPaint {\n  readonly kind: \"ellipse\";\n  readonly cx: number;\n  readonly cy: number;\n  readonly rx: number;\n  readonly ry: number;\n}\n\nexport type ChartMark = RectMark | PathMark | CircleMark | EllipseMark;\n\nexport const num = (value: number): string =>\n  Number.isFinite(value) ? `${Math.round(value * 100) / 100}` : \"0\";\n\n/** The paint attributes a mark carries, as the markup spells them. */\nconst paintAttributes = (mark: ChartMark): string => {\n  const parts: Array<string> = [];\n  parts.push(\n    mark.fill === undefined\n      ? 'fill=\"none\"'\n      : `fill=\"${\n          mark.fill.gradient === undefined\n            ? mark.fill.color\n            : `url(#${mark.fill.gradient})`\n        }\"`,\n  );\n  if (mark.fill?.opacity !== undefined && mark.fill.opacity < 1) {\n    parts.push(`fill-opacity=\"${num(mark.fill.opacity)}\"`);\n  }\n  if (mark.stroke !== undefined) {\n    parts.push(`stroke=\"${mark.stroke.color}\"`);\n    parts.push(`stroke-width=\"${num(mark.stroke.width)}\"`);\n    if (mark.stroke.opacity !== undefined && mark.stroke.opacity < 1) {\n      parts.push(`stroke-opacity=\"${num(mark.stroke.opacity)}\"`);\n    }\n    if (mark.stroke.dash !== undefined) {\n      parts.push(`stroke-dasharray=\"${mark.stroke.dash}\"`);\n    }\n    parts.push('stroke-linecap=\"round\"');\n    parts.push('stroke-linejoin=\"round\"');\n  }\n  return parts.join(\" \");\n};\n\nexport const markMarkup = (mark: ChartMark): string => {\n  const paint = paintAttributes(mark);\n  return Match.value(mark).pipe(\n    Match.when({ kind: \"rect\" }, (shape) => {\n      const radius = shape.rx === undefined ? \"\" : ` rx=\"${num(shape.rx)}\"`;\n      return `<rect x=\"${num(shape.x)}\" y=\"${num(shape.y)}\" width=\"${num(shape.width)}\" height=\"${num(shape.height)}\"${radius} ${paint}/>`;\n    }),\n    Match.when(\n      { kind: \"circle\" },\n      (shape) =>\n        `<circle cx=\"${num(shape.cx)}\" cy=\"${num(shape.cy)}\" r=\"${num(shape.r)}\" ${paint}/>`,\n    ),\n    Match.when(\n      { kind: \"ellipse\" },\n      (shape) =>\n        `<ellipse cx=\"${num(shape.cx)}\" cy=\"${num(shape.cy)}\" rx=\"${num(shape.rx)}\" ry=\"${num(shape.ry)}\" ${paint}/>`,\n    ),\n    Match.when({ kind: \"path\" }, (shape) => `<path d=\"${shape.d}\" ${paint}/>`),\n    Match.exhaustive,\n  );\n};\n\nexport const marksMarkup = (\n  marks: ReadonlyArray<ChartMark>,\n): { readonly defs: string; readonly body: string } => {\n  const indexed: ReadonlyArray<{\n    readonly mark: ChartMark;\n    readonly index: number;\n  }> = Arr.map(marks, (mark, index) => ({ mark, index }));\n  const defs = Arr.join(\n    Arr.flatMap(indexed, (entry) =>\n      entry.mark.clip === undefined\n        ? []\n        : [\n            `<clipPath id=\"chart-clip-${entry.index}\"><rect x=\"${num(entry.mark.clip.x)}\" y=\"${num(entry.mark.clip.y)}\" width=\"${num(entry.mark.clip.width)}\" height=\"${num(entry.mark.clip.height)}\"/></clipPath>`,\n          ],\n    ),\n    \"\",\n  );\n  const body = Arr.join(\n    Arr.map(marks, (mark, index) =>\n      mark.clip === undefined\n        ? markMarkup(mark)\n        : `<g clip-path=\"url(#chart-clip-${index})\">${markMarkup(mark)}</g>`,\n    ),\n    \"\",\n  );\n  return { defs, body };\n};\n\nexport const flatMark = (mark: ChartMark): ChartMark => ({\n  ...mark,\n  fill:\n    mark.fill === undefined ? undefined : { ...mark.fill, gradient: undefined },\n});\n\nexport const FALLBACK_MARK_COLOUR = \"#000000\";\n\nexport const lineStroke = (width: number, dashed = false): MarkStroke => ({\n  color: FALLBACK_MARK_COLOUR,\n  width,\n  dash: dashed ? `${num(width * 2)} ${num(width * 1.6)}` : undefined,\n});\n\nexport const solidFill = (color: string, opacity?: number): MarkFill => ({\n  color,\n  opacity,\n});\n"
    },
    {
      "path": "packages/data/src/chart/paint.ts",
      "target": "@ui/gpuixui/data/chart/paint.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport type { ChartBox } from \"./logic.js\";\nimport type { ChartMark } from \"./mark.js\";\nimport type { ChartDocument, ChartGradient } from \"./svg.js\";\nimport type { PaintLayer } from \"./frame.js\";\n\nexport interface LayerPaint {\n  readonly layer: PaintLayer;\n  /** The colour the layer's marks paint in. */\n  readonly colour: string;\n  /** The layer's own alpha, when it has one. */\n  readonly opacity?: number;\n}\n\nexport const layerMarks = (paint: LayerPaint): ReadonlyArray<ChartMark> =>\n  Arr.map(paint.layer.marks, (mark): ChartMark => {\n    const alpha = paint.opacity;\n    const clip =\n      paint.layer.clip === undefined ? {} : { clip: paint.layer.clip };\n    if (mark.fill === undefined && mark.stroke === undefined) {\n      // the geometry placed is never colourless.\n      return {\n        ...mark,\n        ...clip,\n        fill: { color: paint.colour, opacity: alpha },\n      };\n    }\n    return {\n      ...mark,\n      ...clip,\n      fill:\n        mark.fill === undefined\n          ? undefined\n          : {\n              ...mark.fill,\n              color:\n                mark.fill.color === \"#000000\" ? paint.colour : mark.fill.color,\n              opacity: mark.fill.opacity ?? alpha,\n            },\n      stroke:\n        mark.stroke === undefined\n          ? undefined\n          : {\n              ...mark.stroke,\n              color:\n                mark.stroke.color === \"#000000\"\n                  ? paint.colour\n                  : mark.stroke.color,\n            },\n    };\n  });\n\nexport const gridLayer = (\n  box: ChartBox,\n  tickValues: ReadonlyArray<number>,\n  range: { readonly min: number; readonly max: number },\n  vertical: ReadonlyArray<{ readonly x: number; readonly width: number }>,\n): PaintLayer => ({\n  key: \"grid\",\n  kind: \"grid\",\n  series: { key: \"grid\", label: \"grid\", values: [] },\n  seriesIndex: 0,\n  decorative: true,\n  opacity: 0.55,\n  marks: [\n    ...Arr.map(tickValues, (value) => ({\n      kind: \"rect\" as const,\n      x: 0,\n      y:\n        box.height -\n        ((value - range.min) / Math.max(1e-9, range.max - range.min)) *\n          box.height,\n      width: box.width,\n      height: 1,\n    })),\n    ...Arr.map(vertical, (rule) => ({\n      kind: \"rect\" as const,\n      x: rule.x,\n      y: 0,\n      width: rule.width,\n      height: box.height,\n    })),\n  ],\n});\n\nexport const chartDocument = (\n  box: ChartBox,\n  layers: ReadonlyArray<LayerPaint>,\n  gradients: ReadonlyArray<ChartGradient> = [],\n): ChartDocument => ({\n  box,\n  gradients,\n  marks: Arr.flatten(Arr.map(layers, (paint) => layerMarks(paint))),\n});\n\nexport const areaGradient = (\n  key: string,\n  colour: string,\n  opacity: number,\n): ChartGradient => ({\n  id: `chart-area-${key}`,\n  from: colour,\n  to: colour,\n  fromOpacity: opacity,\n  toOpacity: 0,\n});\n"
    },
    {
      "path": "packages/data/src/chart/plot.tsx",
      "target": "@ui/gpuixui/data/chart/plot.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\n\nimport * as Arr from \"effect/Array\";\nimport { chartDocument, type LayerPaint } from \"./paint.js\";\nimport { ChartMask } from \"./frame.js\";\nimport { ChartSurface, type ChartRender } from \"./surface.js\";\nimport type { ChartGradient } from \"./svg.js\";\nimport type { ChartBox, ChartMark } from \"./logic.js\";\nimport type { PaintLayer } from \"./frame.js\";\n\nexport interface ChartPlotProps {\n  readonly box: ChartBox;\n  readonly layers: ReadonlyArray<PaintLayer>;\n  readonly colours: ReadonlyArray<string>;\n  readonly grid?: PaintLayer;\n  readonly gridColour?: string;\n  readonly gradients?: ReadonlyArray<ChartGradient>;\n  readonly render?: ChartRender;\n  readonly testId?: string;\n  readonly labels?: ReadonlyArray<string>;\n  /** The values each layer's image describes. */\n  readonly descriptions?: ReadonlyArray<string>;\n}\n\nexport const ChartPlot = ({\n  box,\n  layers,\n  colours,\n  grid,\n  gridColour = \"#000000\",\n  gradients = [],\n  render = \"document\",\n  testId,\n  labels = [],\n  descriptions = [],\n}: ChartPlotProps) => {\n  const paints: ReadonlyArray<LayerPaint> = [\n    ...(grid === undefined\n      ? []\n      : [\n          {\n            layer: grid,\n            colour: gridColour,\n            opacity: grid.opacity,\n          },\n        ]),\n    ...Arr.map(layers, (layer, index) => ({\n      layer,\n      colour: colours[index] ?? \"#000000\",\n      opacity: layer.opacity,\n    })),\n  ];\n  if (render === \"mask\") {\n    return (\n      <>\n        {Arr.map(paints, (paint, index) => {\n          // of its own — the grid names nothing, so its slot is empty.\n          const named = grid === undefined || index > 0;\n          return (\n            <ChartMask\n              key={`${paint.layer.key}-${index}`}\n              chartTestId={testId}\n              layer={paint.layer}\n              label={named ? (labels[index - 1] ?? \"\") : \"\"}\n              description={named ? (descriptions[index - 1] ?? \"\") : \"\"}\n              colour={paint.colour}\n              box={box}\n            />\n          );\n        })}\n      </>\n    );\n  }\n  const document = chartDocument(box, paints, gradients);\n  return (\n    <>\n      <ChartSurface\n        document={document}\n        testId={testId === undefined ? undefined : `${testId}-document`}\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          pointerEvents: \"none\",\n        }}\n      />\n      {Arr.map(layers, (layer, index) => (\n        <ChartSeriesName\n          key={`${layer.key}-${layer.kind ?? \"main\"}-name`}\n          chartTestId={testId}\n          layer={layer}\n          label={labels[index] ?? \"\"}\n          description={descriptions[index] ?? \"\"}\n          colour={colours[index] ?? \"#000000\"}\n          box={box}\n        />\n      ))}\n    </>\n  );\n};\n\nexport const ChartSeriesName = ({\n  chartTestId,\n  layer,\n  label,\n  description,\n  colour,\n  box,\n}: {\n  readonly chartTestId: string | undefined;\n  readonly layer: PaintLayer;\n  readonly label: string;\n  readonly description: string;\n  readonly colour: string;\n  readonly box: ChartBox;\n}) => {\n  const decorative = layer.decorative === true;\n  return (\n    <div\n      role={decorative ? undefined : \"img\"}\n      aria-label={decorative ? undefined : label}\n      aria-description={decorative ? undefined : description}\n      testId={\n        chartTestId === undefined\n          ? undefined\n          : layer.kind === undefined\n            ? `${chartTestId}-layer-${layer.key}`\n            : `${chartTestId}-layer-${layer.key}-${layer.kind}`\n      }\n      style={{\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: box.width,\n        height: box.height,\n        color: colour,\n        pointerEvents: \"none\",\n      }}\n    />\n  );\n};\n\n/** The marks a layer paints, for a caller that reads them directly. */\nexport const layerMarkList = (\n  layers: ReadonlyArray<PaintLayer>,\n): ReadonlyArray<ChartMark> =>\n  Arr.flatten(Arr.map(layers, (layer) => layer.marks));\n"
    },
    {
      "path": "packages/data/src/chart/recipe.ts",
      "target": "@ui/gpuixui/data/chart/recipe.ts",
      "type": "registry:lib",
      "content": "export const CHART_ROOT = \"flex flex-col gap-2\";\n\nexport const CHART_PLOT = \"relative shrink-0\";\n\nexport const CHART_AXIS = \"flex flex-col justify-between shrink-0\";\n\nexport const CHART_AXIS_LABEL = \"text-xs text-muted-foreground\";\n\nexport const CHART_CATEGORIES = \"flex flex-row shrink-0\";\n\nexport const CHART_CATEGORY =\n  \"flex flex-row justify-center overflow-hidden text-xs text-muted-foreground\";\n\nexport const CHART_CATEGORY_LABEL =\n  \"text-xs text-muted-foreground text-ellipsis whitespace-nowrap\";\n\nexport const CHART_LEGEND = \"flex flex-row flex-wrap items-center gap-3\";\n\nexport const CHART_LEGEND_ITEM = \"flex flex-row items-center gap-1.5\";\n\nexport const CHART_SWATCH = \"rounded-sm shrink-0\";\n\nexport const CHART_TOOLTIP_ZONE = \"shrink-0\";\n\nexport const CHART_TOOLTIP =\n  \"flex flex-col gap-1 rounded-md border border-border bg-popover p-2\";\n\nexport const CHART_TOOLTIP_HEAD = \"text-xs\";\n\nexport const CHART_TOOLTIP_ROW = \"flex flex-row items-center gap-1\";\n\nexport const CHART_TOOLTIP_VALUE = \"text-xs\";\n\nexport const CHART_VALUE_LABEL = \"text-xs\";\n\nexport const CHART_BRUSH = \"shrink-0\";\n\nexport const CHART_BRUSH_TRACK =\n  \"relative h-4 overflow-hidden rounded-sm bg-muted pointer-events-none\";\n\nexport const CHART_BRUSH_BAND =\n  \"top-0 bottom-0 rounded-sm opacity-40 pointer-events-none\";\n\nexport const CHART_BRUSH_HANDLE =\n  \"top-0 bottom-0 w-1 rounded-sm pointer-events-none\";\n"
    },
    {
      "path": "packages/data/src/chart/scene.ts",
      "target": "@ui/gpuixui/data/chart/scene.ts",
      "type": "registry:lib",
      "content": "import { areaY } from \"@tanstack/charts/area\";\nimport { barY } from \"@tanstack/charts/bar\";\nimport { lineY } from \"@tanstack/charts/line\";\nimport { treemap } from \"@tanstack/charts/hierarchy/treemap\";\nimport { sunburst } from \"@tanstack/charts/hierarchy/sunburst\";\nimport { sankeyDiagram } from \"@tanstack/charts/network/sankey\";\nimport { link } from \"@tanstack/charts/link\";\nimport { polar, radialBarRadius } from \"@tanstack/charts/polar\";\nimport { rect } from \"@tanstack/charts/rect\";\nimport { scaleBand } from \"@tanstack/charts/scales/band\";\nimport { scaleLinear } from \"@tanstack/charts/scales/linear\";\nimport { createChartScene, defineChart } from \"@tanstack/charts/scene\";\nimport { tickX, tickY } from \"@tanstack/charts/tick\";\nimport { renderChartSvg } from \"@tanstack/charts/svg\";\nimport type {\n  ChartPoint,\n  ChartScene,\n  ChartSpec,\n  StaticChartDefinition,\n} from \"@tanstack/charts/types/core\";\nimport { Match } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport type { ChartBox, ChartSeries } from \"./logic.js\";\nimport type { ChartRange } from \"./geometry.js\";\nimport {\n  HIERARCHY_SEPARATOR,\n  type FlowLink,\n  type FlowNode,\n  type HierarchyRow,\n} from \"./hierarchy.js\";\n\nexport type SceneChartKind =\n  | \"bar\"\n  | \"line\"\n  | \"area\"\n  | \"treemap\"\n  | \"sankey\"\n  | \"sunburst\"\n  | \"radial\";\n\nexport interface SceneRow {\n  readonly category: string;\n  readonly [series: string]: string | number;\n}\n\nexport interface HierarchySceneRow extends SceneRow {\n  readonly path: string;\n  readonly name: string;\n  readonly value: number;\n}\n\nexport interface SceneChartOptions {\n  readonly series: ReadonlyArray<ChartSeries>;\n  readonly labels: ReadonlyArray<string>;\n  readonly box: ChartBox;\n  readonly range: ChartRange;\n  readonly colours: ReadonlyArray<string>;\n  readonly kind: SceneChartKind;\n  readonly label: string;\n  readonly description: string;\n  readonly axis?: boolean;\n  readonly xLabels?: boolean;\n  readonly hierarchy?: ReadonlyArray<HierarchyRow>;\n  readonly flowNodes?: ReadonlyArray<FlowNode>;\n  readonly flowLinks?: ReadonlyArray<FlowLink>;\n  readonly nodeColours?: ReadonlyMap<string, string>;\n  readonly method?: \"squarify\" | \"binary\" | \"dice\" | \"slice\" | \"slice-dice\";\n  readonly ratio?: number;\n  readonly paddingInner?: number;\n  readonly paddingOuter?: number;\n  readonly nodeWidth?: number;\n  readonly nodePadding?: number;\n  readonly innerRadius?: number;\n  readonly ringPadding?: number;\n  readonly visibleDepth?: number;\n  readonly radiusRatio?: number;\n  readonly bandPadding?: number;\n  readonly reveal?: number;\n}\n\nexport const sceneRows = (\n  series: ReadonlyArray<ChartSeries>,\n  labels: ReadonlyArray<string>,\n): ReadonlyArray<SceneRow> =>\n  Arr.map(labels, (category, index) => ({\n    category,\n    ...Object.fromEntries(\n      Arr.map(series, (one) => [one.key, one.values[index] ?? 0]),\n    ),\n  }));\n\nconst valueChannel =\n  (key: string) =>\n  (row: SceneRow): number => {\n    const value = row[key];\n    return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n  };\n\nconst linearDomain = (range: ChartRange): readonly [number, number] => [\n  range.min,\n  range.max === range.min ? range.min + 1 : range.max,\n];\n\nconst seriesMark = (\n  kind: \"bar\" | \"line\" | \"area\",\n  rows: ReadonlyArray<SceneRow>,\n  series: ChartSeries,\n  colour: string,\n) => {\n  const channels = {\n    x: \"category\" as const,\n    y: valueChannel(series.key),\n    key: (row: SceneRow) => `${series.key}:${String(row.category)}`,\n  };\n  return Match.value(kind).pipe(\n    Match.when(\"bar\", () =>\n      barY(rows, { ...channels, fill: colour, radius: 3 }),\n    ),\n    Match.when(\"line\", () =>\n      lineY(rows, { ...channels, stroke: colour, strokeWidth: 2 }),\n    ),\n    Match.when(\"area\", () =>\n      areaY(rows, {\n        ...channels,\n        fill: colour,\n        fillOpacity: 0.35,\n        stroke: colour,\n        strokeWidth: 2,\n      }),\n    ),\n    Match.exhaustive,\n  );\n};\n\nconst treemapMark = (options: SceneChartOptions) =>\n  treemap(options.hierarchy ?? [], {\n    value: (row: HierarchyRow) => row.value,\n    path: (row: HierarchyRow) => row.path,\n    delimiter: HIERARCHY_SEPARATOR,\n    fill: (node) =>\n      options.nodeColours?.get(node.data?.key ?? node.id) ?? \"#000000\",\n    ...(options.method === undefined ? {} : { method: options.method }),\n    ...(options.ratio === undefined ? {} : { ratio: options.ratio }),\n    ...(options.paddingInner === undefined\n      ? {}\n      : { paddingInner: options.paddingInner }),\n    ...(options.paddingOuter === undefined\n      ? {}\n      : { paddingOuter: options.paddingOuter }),\n  });\n\nconst sunburstMark = (options: SceneChartOptions) =>\n  polar({\n    startAngle: Math.PI / 2,\n    endAngle: Math.PI / 2 - Math.PI * 2,\n    marks: [\n      sunburst(options.hierarchy ?? [], {\n        value: (row: HierarchyRow) => row.value,\n        path: (row: HierarchyRow) => row.path,\n        delimiter: HIERARCHY_SEPARATOR,\n        fill: (node) =>\n          options.nodeColours?.get(node.data?.key ?? node.id) ?? \"#000000\",\n        ...(options.innerRadius === undefined\n          ? {}\n          : { innerRadius: options.innerRadius }),\n        ...(options.ringPadding === undefined\n          ? {}\n          : { ringPadding: options.ringPadding }),\n        ...(options.visibleDepth === undefined\n          ? {}\n          : { visibleDepth: options.visibleDepth }),\n      }),\n    ],\n    scales: { angle: null, radius: null },\n  });\n\nconst sankeyMark = (options: SceneChartOptions) =>\n  sankeyDiagram({\n    nodes: options.flowNodes ?? [],\n    links: options.flowLinks ?? [],\n    nodeKey: (node: FlowNode) => node.id,\n    source: (edge: FlowLink) => edge.source,\n    target: (edge: FlowLink) => edge.target,\n    value: (edge: FlowLink) => edge.value,\n    ...(options.nodeWidth === undefined\n      ? {}\n      : { nodeWidth: options.nodeWidth }),\n    ...(options.nodePadding === undefined\n      ? {}\n      : { nodePadding: options.nodePadding }),\n    marks: (context) => [\n      link(context.links, {\n        x1: \"x1\",\n        y1: \"y1\",\n        x2: \"x2\",\n        y2: \"y2\",\n        key: \"key\",\n        stroke: options.colours[0] ?? \"#000000\",\n        strokeOpacity: 0.35,\n        strokeWidth: (edge) => edge.width * (options.reveal ?? 1),\n      }),\n      ...context.nodes.map((node) =>\n        rect([node], {\n          x1: \"x0\",\n          x2: \"x1\",\n          y1: \"y0\",\n          y2: \"y1\",\n          key: \"key\",\n          inset: 0,\n          fill:\n            options.nodeColours?.get(node.data.id) ??\n            options.colours[0] ??\n            \"#000000\",\n        }),\n      ),\n    ],\n  });\n\nconst treemapDefinition = (options: SceneChartOptions) =>\n  defineChart<\n    readonly [ReturnType<typeof treemapMark>],\n    ChartSpec<readonly [ReturnType<typeof treemapMark>]>\n  >({\n    marks: [treemapMark(options)],\n    scales: { x: null, y: null },\n  });\n\nconst cartesianDefinition = (\n  options: SceneChartOptions,\n): StaticChartDefinition<SceneRow> => {\n  const rows = sceneRows(options.series, options.labels);\n  const kind = options.kind as \"bar\" | \"line\" | \"area\";\n  const marks = Arr.map(options.series, (one, index) =>\n    seriesMark(kind, rows, one, options.colours[index] ?? \"#000000\"),\n  );\n  // at a value, and the value axis is one scale. A chart with no series has\n  const measure = options.series[0];\n  const ticks =\n    measure === undefined\n      ? []\n      : [\n          ...(options.axis === false\n            ? []\n            : [\n                tickY(rows, {\n                  x: \"category\",\n                  y: valueChannel(measure.key),\n                  key: \"tick-y\",\n                }),\n              ]),\n          ...(options.xLabels === false\n            ? []\n            : [\n                tickX(rows, {\n                  x: \"category\",\n                  y: valueChannel(measure.key),\n                  key: \"tick-x\",\n                }),\n              ]),\n        ];\n  return defineChart({\n    marks: [...marks, ...ticks],\n    scales: {\n      x: { scale: scaleBand().domain(options.labels) },\n      y: { scale: scaleLinear().domain(linearDomain(options.range)) },\n    },\n  });\n};\n\nconst radialBarMark = (options: SceneChartOptions) =>\n  polar({\n    radiusRatio: options.radiusRatio ?? 0.8,\n    scales: {\n      angle: {\n        scale: () => scaleBand<string>().padding(options.bandPadding ?? 0.12),\n      },\n      radius: {\n        scale: scaleLinear().domain([0, options.range.max]),\n        range: [({ radius }) => radius * 0.3, ({ radius }) => radius],\n      },\n    },\n    marks: [\n      radialBarRadius(sceneRows(options.series, options.labels), {\n        angle: \"category\",\n        // draws one bar per category, so there is one value per category.\n        radius: valueChannel(options.series[0]?.key ?? \"value\"),\n        key: \"category\",\n        fill: options.colours[0] ?? \"#000000\",\n      }),\n    ],\n  });\n\nconst sankeyDefinition = (options: SceneChartOptions) =>\n  defineChart<\n    readonly [ReturnType<typeof sankeyMark>],\n    ChartSpec<readonly [ReturnType<typeof sankeyMark>]>\n  >({\n    marks: [sankeyMark(options)],\n    scales: { x: null, y: null },\n  });\n\nconst sunburstDefinition = (options: SceneChartOptions) =>\n  defineChart<\n    readonly [ReturnType<typeof sunburstMark>],\n    ChartSpec<readonly [ReturnType<typeof sunburstMark>]>\n  >({\n    marks: [sunburstMark(options)],\n    scales: { x: null, y: null },\n  });\n\nconst radialDefinition = (options: SceneChartOptions) =>\n  defineChart<\n    readonly [ReturnType<typeof radialBarMark>],\n    ChartSpec<readonly [ReturnType<typeof radialBarMark>]>\n  >({\n    marks: [radialBarMark(options)],\n    scales: { x: null, y: null },\n  });\n\nexport const sceneDefinition = (\n  options: SceneChartOptions,\n): StaticChartDefinition<SceneRow> => {\n  // cartesian mark and a polar one have no common type.\n  if (options.kind === \"treemap\") {\n    return treemapDefinition(options) as StaticChartDefinition<SceneRow>;\n  }\n  if (options.kind === \"sankey\") {\n    return sankeyDefinition(options) as StaticChartDefinition<SceneRow>;\n  }\n  if (options.kind === \"sunburst\") {\n    return sunburstDefinition(options) as StaticChartDefinition<SceneRow>;\n  }\n  if (options.kind === \"radial\") {\n    return radialDefinition(options) as StaticChartDefinition<SceneRow>;\n  }\n  return cartesianDefinition(options);\n};\n\nexport const chartScene = (options: SceneChartOptions): ChartScene<SceneRow> =>\n  createChartScene(sceneDefinition(options), {\n    width: options.box.width,\n    height: options.box.height,\n  });\n\nexport const sceneSvg = (options: SceneChartOptions): string =>\n  renderChartSvg(chartScene(options), {\n    ariaLabel: options.label,\n    ariaDescription: options.description,\n  });\n\nexport const scenePointAt = (\n  scene: ChartScene<SceneRow>,\n  x: number,\n  y: number,\n  maxDistance = 24,\n): Option.Option<ChartPoint<SceneRow>> => {\n  const points = scene.points;\n  if (points.length === 0) return Option.none();\n  return Option.fromNullishOr(\n    Arr.reduce(\n      points,\n      undefined as ChartPoint<SceneRow> | undefined,\n      (best, point) => {\n        const distance = Math.hypot(point.x - x, point.y - y);\n        if (distance > maxDistance) return best;\n        if (best === undefined) return point;\n        const bestDistance = Math.hypot(best.x - x, best.y - y);\n        return distance < bestDistance ? point : best;\n      },\n    ),\n  );\n};\n"
    },
    {
      "path": "packages/data/src/chart/structure.tsx",
      "target": "@ui/gpuixui/data/chart/structure.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\n\nimport { useMemo } from \"react\";\nimport * as Arr from \"effect/Array\";\nimport { ChartLegend } from \"./legend.js\";\nimport { ChartSurface } from \"./surface.js\";\nimport {\n  flowGraph,\n  hierarchyRows,\n  type FlowLink,\n  type FlowNode,\n  type HierarchyNode,\n  type HierarchyRow,\n} from \"./hierarchy.js\";\nimport { sceneSvg, type SceneChartKind } from \"./scene.js\";\nimport type { ChartBox, ChartSeries } from \"./logic.js\";\nimport type { ChartRange } from \"./geometry.js\";\nimport {\n  DEFAULT_ANIMATION_DURATION,\n  scaleValue,\n  useAnimatedValues,\n  useMountReveal,\n} from \"./animation.js\";\n\nexport const nodeColours = (\n  rows: ReadonlyArray<HierarchyRow>,\n  colours: ReadonlyArray<string>,\n  named?: ReadonlyMap<string, string>,\n): ReadonlyMap<string, string> => {\n  const map = new Map<string, string>();\n  let index = 0;\n  for (const row of rows) {\n    if (map.has(row.key)) continue;\n    map.set(\n      row.key,\n      named?.get(row.key) ??\n        colours[index % Math.max(1, colours.length)] ??\n        \"#000000\",\n    );\n    index += 1;\n  }\n  return map;\n};\n\nconst sankeyColours = (\n  graph: { readonly nodes: ReadonlyArray<FlowNode> },\n  colours: ReadonlyArray<string>,\n  named?: ReadonlyMap<string, string>,\n): ReadonlyMap<string, string> => {\n  const map = new Map<string, string>();\n  Arr.forEach(graph.nodes, (node, index) => {\n    map.set(\n      node.id,\n      named?.get(node.id) ??\n        colours[index % Math.max(1, colours.length)] ??\n        \"#000000\",\n    );\n  });\n  return map;\n};\n\nconst SceneChart = ({\n  kind,\n  rows,\n  flow,\n  colours,\n  named,\n  range,\n  series,\n  labels,\n  size,\n  label,\n  description,\n  method,\n  ratio,\n  paddingInner,\n  paddingOuter,\n  nodeWidth,\n  nodePadding,\n  innerRadius,\n  ringPadding,\n  visibleDepth,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  testId,\n}: {\n  readonly kind: SceneChartKind;\n  readonly rows: ReadonlyArray<HierarchyRow>;\n  readonly flow: {\n    readonly nodes: ReadonlyArray<FlowNode>;\n    readonly links: ReadonlyArray<FlowLink>;\n  };\n  readonly colours: ReadonlyArray<string>;\n  readonly named?: ReadonlyMap<string, string>;\n  readonly range?: ChartRange;\n  readonly series?: ReadonlyArray<ChartSeries>;\n  readonly labels?: ReadonlyArray<string>;\n  readonly size: number;\n  readonly label: string;\n  readonly description: string;\n  readonly method?: \"squarify\" | \"binary\" | \"dice\" | \"slice\" | \"slice-dice\";\n  readonly ratio?: number;\n  readonly paddingInner?: number;\n  readonly paddingOuter?: number;\n  readonly nodeWidth?: number;\n  readonly nodePadding?: number;\n  readonly innerRadius?: number;\n  readonly ringPadding?: number;\n  readonly visibleDepth?: number;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly testId?: string;\n}) => {\n  const box: ChartBox = { width: size, height: size };\n  const reveal = useMountReveal(animate, animationDuration);\n  const hierarchyTween =\n    useAnimatedValues(\n      [Arr.map(rows, (row) => row.value)],\n      animate,\n      animationDuration,\n    )[0] ?? [];\n  const linkTween =\n    useAnimatedValues(\n      [Arr.map(flow.links, (link) => link.value)],\n      animate,\n      animationDuration,\n    )[0] ?? [];\n  const seriesTween = useAnimatedValues(\n    Arr.map(series ?? [], (one) => one.values),\n    animate,\n    animationDuration,\n  );\n  const shownRows = Arr.map(rows, (row, index) => ({\n    ...row,\n    value: scaleValue(hierarchyTween[index] ?? row.value, reveal),\n  }));\n  const tweenedLinks = Arr.map(flow.links, (link, index) => ({\n    ...link,\n    value: linkTween[index] ?? link.value,\n  }));\n  // A sankey cannot use zero link values: its compiler rejects them and\n  // normalises ribbons to the layout, so `reveal` scales ribbon width instead.\n  const shownSeries = Arr.map(series ?? [], (one, index) => ({\n    ...one,\n    values: Arr.map(one.values, (value, at) =>\n      scaleValue(seriesTween[index]?.[at] ?? value, reveal),\n    ),\n  }));\n  const markup = useMemo(\n    () =>\n      sceneSvg({\n        series: shownSeries,\n        labels: labels ?? [],\n        box,\n        range: range ?? { min: 0, max: 1 },\n        colours,\n        kind,\n        label,\n        description,\n        hierarchy: shownRows,\n        flowNodes: flow.nodes,\n        flowLinks: tweenedLinks,\n        nodeColours: named ?? nodeColours(rows, colours),\n        reveal,\n        ...(method === undefined ? {} : { method }),\n        ...(ratio === undefined ? {} : { ratio }),\n        ...(paddingInner === undefined ? {} : { paddingInner }),\n        ...(paddingOuter === undefined ? {} : { paddingOuter }),\n        ...(nodeWidth === undefined ? {} : { nodeWidth }),\n        ...(nodePadding === undefined ? {} : { nodePadding }),\n        ...(innerRadius === undefined ? {} : { innerRadius }),\n        ...(ringPadding === undefined ? {} : { ringPadding }),\n        ...(visibleDepth === undefined ? {} : { visibleDepth }),\n      }),\n    [\n      box.width,\n      box.height,\n      colours,\n      range?.min,\n      range?.max,\n      shownSeries,\n      labels,\n      kind,\n      label,\n      description,\n      shownRows,\n      flow.nodes,\n      tweenedLinks,\n      reveal,\n      method,\n      ratio,\n      paddingInner,\n      paddingOuter,\n      nodeWidth,\n      nodePadding,\n      innerRadius,\n      ringPadding,\n      visibleDepth,\n    ],\n  );\n  return (\n    <ChartSurface\n      document={{ box, marks: [] }}\n      markup={markup}\n      label={label}\n      description={description}\n      testId={testId === undefined ? undefined : `${testId}-document`}\n      style={{ position: \"absolute\", left: 0, top: 0, pointerEvents: \"none\" }}\n    />\n  );\n};\n\nexport const leafRows = (\n  rows: ReadonlyArray<HierarchyRow>,\n): ReadonlyArray<HierarchyRow> =>\n  rows.filter(\n    (row) =>\n      !Arr.some(\n        rows,\n        (other) => other.key !== row.key && other.key.startsWith(`${row.key}/`),\n      ),\n  );\n\nexport interface TreemapChartProps {\n  readonly nodes: ReadonlyArray<HierarchyNode>;\n  readonly size?: number;\n  readonly method?: \"squarify\" | \"binary\" | \"dice\" | \"slice\" | \"slice-dice\";\n  readonly ratio?: number;\n  readonly paddingInner?: number;\n  readonly paddingOuter?: number;\n  readonly colors?: ReadonlyArray<string>;\n  readonly nodeColors?: ReadonlyMap<string, string>;\n  readonly legend?: boolean;\n  readonly valueFormat?: (value: number) => string;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly testId?: string;\n  readonly label?: string;\n}\n\nexport const TreemapChart = ({\n  nodes,\n  size = 200,\n  method,\n  ratio,\n  paddingInner,\n  paddingOuter,\n  colors,\n  nodeColors,\n  legend = true,\n  valueFormat,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  testId,\n  label = \"Treemap\",\n}: TreemapChartProps) => {\n  const rows = hierarchyRows(nodes);\n  const leaves = leafRows(rows);\n  const palette = colors ?? [];\n  const description = Arr.map(leaves, (row) => `${row.name} ${row.value}`).join(\n    \", \",\n  );\n  return (\n    <div role=\"group\" testId={testId} style={{ position: \"relative\" }}>\n      <SceneChart\n        kind=\"treemap\"\n        rows={rows}\n        flow={{ nodes: [], links: [] }}\n        colours={palette}\n        size={size}\n        label={label}\n        description={description}\n        animate={animate}\n        animationDuration={animationDuration}\n        {...(method === undefined ? {} : { method })}\n        {...(ratio === undefined ? {} : { ratio })}\n        {...(paddingInner === undefined ? {} : { paddingInner })}\n        {...(paddingOuter === undefined ? {} : { paddingOuter })}\n        testId={testId}\n      />\n      {legend ? (\n        <ChartLegend\n          testId={testId === undefined ? undefined : `${testId}-legend`}\n          items={Arr.map(leaves, (row, index) => ({\n            key: row.key,\n            label:\n              valueFormat === undefined\n                ? row.name\n                : `${row.name} · ${valueFormat(row.value)}`,\n            color:\n              nodeColors?.get(row.key) ??\n              palette[index % Math.max(1, palette.length)] ??\n              \"#000000\",\n          }))}\n        />\n      ) : undefined}\n    </div>\n  );\n};\n\nexport interface RadialBarChartProps {\n  readonly data: ReadonlyArray<{\n    readonly key: string;\n    readonly label?: string;\n    readonly value: number;\n  }>;\n  readonly size?: number;\n  readonly radiusRatio?: number;\n  readonly bandPadding?: number;\n  readonly colors?: ReadonlyArray<string>;\n  readonly legend?: boolean;\n  readonly valueFormat?: (value: number) => string;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly testId?: string;\n  readonly label?: string;\n}\n\nexport const RadialBarChart = ({\n  data,\n  size = 200,\n  radiusRatio,\n  bandPadding,\n  colors,\n  legend = true,\n  valueFormat,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  testId,\n  label = \"Radial bar\",\n}: RadialBarChartProps) => {\n  const palette = colors ?? [];\n  const rows = Arr.map(data, (row) => ({\n    category: row.label ?? row.key,\n    value: row.value,\n  }));\n  const description = Arr.map(\n    rows,\n    (row) => `${row.category} ${row.value}`,\n  ).join(\", \");\n  const largest = Arr.reduce(rows, 0, (max, row) => Math.max(max, row.value));\n  return (\n    <div role=\"group\" testId={testId} style={{ position: \"relative\" }}>\n      <SceneChart\n        kind=\"radial\"\n        rows={[]}\n        flow={{ nodes: [], links: [] }}\n        colours={palette}\n        size={size}\n        label={label}\n        description={description}\n        animate={animate}\n        animationDuration={animationDuration}\n        range={{ min: 0, max: largest }}\n        series={[\n          {\n            key: \"value\",\n            label: \"Value\",\n            values: Arr.map(rows, (row) => row.value),\n          },\n        ]}\n        labels={Arr.map(rows, (row) => row.category)}\n        {...(radiusRatio === undefined ? {} : { radiusRatio })}\n        {...(bandPadding === undefined ? {} : { bandPadding })}\n        testId={testId}\n      />\n      {legend ? (\n        <ChartLegend\n          testId={testId === undefined ? undefined : `${testId}-legend`}\n          items={Arr.map(rows, (row, index) => ({\n            key: row.category,\n            label:\n              valueFormat === undefined\n                ? row.category\n                : `${row.category} · ${valueFormat(row.value)}`,\n            color: palette[index % Math.max(1, palette.length)] ?? \"#000000\",\n          }))}\n        />\n      ) : undefined}\n    </div>\n  );\n};\n\nexport interface SankeyChartProps {\n  readonly nodes: ReadonlyArray<FlowNode>;\n  readonly links: ReadonlyArray<FlowLink>;\n  readonly size?: number;\n  readonly nodeWidth?: number;\n  readonly nodePadding?: number;\n  readonly colors?: ReadonlyArray<string>;\n  readonly nodeColors?: ReadonlyMap<string, string>;\n  readonly legend?: boolean;\n  readonly valueFormat?: (value: number) => string;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly testId?: string;\n  readonly label?: string;\n}\n\nexport const SankeyChart = ({\n  nodes,\n  links,\n  size = 200,\n  nodeWidth,\n  nodePadding,\n  colors,\n  nodeColors,\n  legend = true,\n  valueFormat,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  testId,\n  label = \"Sankey\",\n}: SankeyChartProps) => {\n  const graph = flowGraph(nodes, links);\n  const palette = colors ?? [];\n  const description = Arr.map(\n    graph.links,\n    (link) => `${link.source} to ${link.target} ${link.value}`,\n  ).join(\", \");\n  return (\n    <div role=\"group\" testId={testId} style={{ position: \"relative\" }}>\n      <SceneChart\n        kind=\"sankey\"\n        rows={[]}\n        flow={graph}\n        colours={palette}\n        named={sankeyColours(graph, palette, nodeColors)}\n        size={size}\n        label={label}\n        description={description}\n        animate={animate}\n        animationDuration={animationDuration}\n        {...(nodeWidth === undefined ? {} : { nodeWidth })}\n        {...(nodePadding === undefined ? {} : { nodePadding })}\n        testId={testId}\n      />\n      {legend ? (\n        <ChartLegend\n          testId={testId === undefined ? undefined : `${testId}-legend`}\n          items={Arr.map(graph.nodes, (node, index) => ({\n            key: node.id,\n            label: node.name ?? node.id,\n            value: (() => {\n              const through = Arr.filter(\n                graph.links,\n                (link) => link.source === node.id || link.target === node.id,\n              );\n              const total = Arr.reduce(\n                through,\n                0,\n                (sum, link) => sum + link.value,\n              );\n              return valueFormat === undefined\n                ? String(total)\n                : valueFormat(total);\n            })(),\n            color:\n              nodeColors?.get(node.id) ??\n              palette[index % Math.max(1, palette.length)] ??\n              \"#000000\",\n          }))}\n        />\n      ) : undefined}\n    </div>\n  );\n};\n\nexport interface SunburstChartProps {\n  readonly nodes: ReadonlyArray<HierarchyNode>;\n  readonly size?: number;\n  readonly innerRadius?: number;\n  readonly ringPadding?: number;\n  readonly visibleDepth?: number;\n  readonly colors?: ReadonlyArray<string>;\n  readonly nodeColors?: ReadonlyMap<string, string>;\n  readonly legend?: boolean;\n  readonly valueFormat?: (value: number) => string;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly testId?: string;\n  readonly label?: string;\n}\n\nexport const SunburstChart = ({\n  nodes,\n  size = 200,\n  innerRadius,\n  ringPadding,\n  visibleDepth,\n  colors,\n  nodeColors,\n  legend = true,\n  valueFormat,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  testId,\n  label = \"Sunburst\",\n}: SunburstChartProps) => {\n  const rows = hierarchyRows(nodes);\n  const leaves = leafRows(rows);\n  const palette = colors ?? [];\n  const description = Arr.map(leaves, (row) => `${row.name} ${row.value}`).join(\n    \", \",\n  );\n  return (\n    <div role=\"group\" testId={testId} style={{ position: \"relative\" }}>\n      <SceneChart\n        kind=\"sunburst\"\n        rows={rows}\n        flow={{ nodes: [], links: [] }}\n        colours={palette}\n        size={size}\n        label={label}\n        description={description}\n        animate={animate}\n        animationDuration={animationDuration}\n        {...(innerRadius === undefined ? {} : { innerRadius })}\n        {...(ringPadding === undefined ? {} : { ringPadding })}\n        {...(visibleDepth === undefined ? {} : { visibleDepth })}\n        testId={testId}\n      />\n      {legend ? (\n        <ChartLegend\n          testId={testId === undefined ? undefined : `${testId}-legend`}\n          items={Arr.map(leaves, (row, index) => ({\n            key: row.key,\n            label:\n              valueFormat === undefined\n                ? row.name\n                : `${row.name} · ${valueFormat(row.value)}`,\n            color:\n              nodeColors?.get(row.key) ??\n              palette[index % Math.max(1, palette.length)] ??\n              \"#000000\",\n          }))}\n        />\n      ) : undefined}\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/chart/surface.tsx",
      "target": "@ui/gpuixui/data/chart/surface.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { ImgInstance, Props } from \"@gpuix/react\";\nimport { Effect } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as Schema from \"effect/Schema\";\nimport { useLayoutEffect, useRef } from \"react\";\nimport type { ChartBox } from \"./logic.js\";\nimport { chartSvg, type ChartDocument } from \"./svg.js\";\n\nexport type ChartRender = \"document\" | \"mask\";\n\nexport class ChartDecodeError extends Schema.TaggedError<ChartDecodeError>()(\n  \"ChartDecodeError\",\n  { reason: Schema.String },\n  { description: \"The chart's SVG could not be decoded by the runtime.\" },\n) {}\n\nexport const documentDataUrl = (markup: string): string =>\n  `data:image/svg+xml;base64,${base64(markup)}`;\n\nconst base64 = (value: string): string => {\n  if (typeof Buffer !== \"undefined\") {\n    return Buffer.from(value, \"utf8\").toString(\"base64\");\n  }\n  const bytes = new TextEncoder().encode(value);\n  return btoa(\n    Arr.join(\n      Arr.map(Array.from(bytes), (byte) => String.fromCharCode(byte)),\n      \"\",\n    ),\n  );\n};\n\nexport const pushDocument = (\n  element: ImgInstance | null,\n  markup: string,\n): Effect.Effect<void, ChartDecodeError> =>\n  element === null\n    ? Effect.void\n    : Effect.try({\n        try: () => element.setImage(Buffer.from(markup, \"utf8\")),\n        catch: (cause) => new ChartDecodeError({ reason: String(cause) }),\n      });\n\nexport interface ChartSurfaceProps extends Omit<Props, \"children\"> {\n  readonly document: ChartDocument;\n\n  readonly markup?: string;\n\n  readonly render?: ChartRender;\n\n  readonly label?: string;\n\n  readonly description?: string;\n\n  readonly testId?: string;\n\n  readonly onDecodeError?: (failure: ChartDecodeError) => void;\n}\n\nexport const ChartSurface = ({\n  document,\n  markup,\n  render = \"document\",\n  label,\n  description,\n  testId,\n  onDecodeError,\n  style,\n  ...rest\n}: ChartSurfaceProps) => {\n  const instance = useRef<ImgInstance | null>(null);\n\n  const drawn = markup ?? chartSvg(document);\n  const box: ChartBox = document.box;\n  const source = render === \"document\" ? documentDataUrl(drawn) : undefined;\n\n  useLayoutEffect(() => {\n    if (render !== \"document\") return;\n    Effect.runSync(\n      pushDocument(instance.current, drawn).pipe(\n        Effect.catchTag(\"ChartDecodeError\", (failure) =>\n          Effect.sync(() => onDecodeError?.(failure)),\n        ),\n      ),\n    );\n  }, [drawn, render, onDecodeError]);\n\n  return (\n    <img\n      {...rest}\n      ref={instance}\n      src={source}\n      role=\"none\"\n      aria-label={label}\n      aria-description={description}\n      testId={testId}\n      style={{\n        width: box.width,\n        height: box.height,\n        ...style,\n      }}\n    />\n  );\n};\n\nexport const ChartMaskSurface = ({\n  document,\n  label,\n  description,\n  testId,\n  style,\n  ...rest\n}: Omit<ChartSurfaceProps, \"render\">) => {\n  const box = document.box;\n  return (\n    <svg\n      {...rest}\n      source={chartSvg(document)}\n      role={label === undefined ? undefined : \"img\"}\n      aria-label={label}\n      aria-description={description}\n      testId={testId}\n      style={{\n        width: box.width,\n        height: box.height,\n        ...style,\n      }}\n    />\n  );\n};\n\nexport const rendersDocument = (render: ChartRender | undefined): boolean =>\n  Option.getOrElse(Option.fromNullishOr(render), () => \"document\" as const) ===\n  \"document\";\n"
    },
    {
      "path": "packages/data/src/chart/svg.ts",
      "target": "@ui/gpuixui/data/chart/svg.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport type { ChartBox } from \"./logic.js\";\nimport type { ChartMark } from \"./mark.js\";\nimport { marksMarkup, num } from \"./mark.js\";\n\nexport interface ChartGradient {\n  readonly id: string;\n  readonly from: string;\n  readonly to: string;\n  readonly direction?: \"vertical\" | \"horizontal\";\n  readonly fromOpacity?: number;\n  readonly toOpacity?: number;\n}\n\nexport interface ChartDocument {\n  /** The plot's box, in pixels. */\n  readonly box: ChartBox;\n  /** The marks, back to front. */\n  readonly marks: ReadonlyArray<ChartMark>;\n  /** The gradients the marks refer to. */\n  readonly gradients?: ReadonlyArray<ChartGradient>;\n  readonly scale?: number;\n}\n\nexport const gradientMarkup = (gradient: ChartGradient): string => {\n  const horizontal = gradient.direction === \"horizontal\";\n  const x1 = horizontal ? \"0%\" : \"0%\";\n  const x2 = horizontal ? \"100%\" : \"0%\";\n  const y1 = horizontal ? \"0%\" : \"0%\";\n  const y2 = horizontal ? \"0%\" : \"100%\";\n  const stops = [\n    `<stop offset=\"0%\" stop-color=\"${gradient.from}\"${\n      gradient.fromOpacity === undefined\n        ? \"\"\n        : ` stop-opacity=\"${num(gradient.fromOpacity)}\"`\n    }/>`,\n    `<stop offset=\"100%\" stop-color=\"${gradient.to}\"${\n      gradient.toOpacity === undefined\n        ? \"\"\n        : ` stop-opacity=\"${num(gradient.toOpacity)}\"`\n    }/>`,\n  ].join(\"\");\n  return `<linearGradient id=\"${gradient.id}\" x1=\"${x1}\" y1=\"${y1}\" x2=\"${x2}\" y2=\"${y2}\">${stops}</linearGradient>`;\n};\n\nexport const defsMarkup = (\n  gradients: ReadonlyArray<ChartGradient>,\n  clips = \"\",\n): string =>\n  gradients.length === 0 && clips.length === 0\n    ? \"\"\n    : `<defs>${Arr.join(Arr.map(gradients, gradientMarkup), \"\")}${clips}</defs>`;\n\nexport const chartSvg = (document: ChartDocument): string => {\n  const scale =\n    document.scale === undefined || !Number.isFinite(document.scale)\n      ? 1\n      : Math.max(0.1, document.scale);\n  const width = document.box.width * scale;\n  const height = document.box.height * scale;\n  const viewBox = `0 0 ${num(document.box.width)} ${num(document.box.height)}`;\n  const marks = marksMarkup(document.marks);\n  return [\n    `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${num(width)}\" height=\"${num(height)}\" viewBox=\"${viewBox}\">`,\n    defsMarkup(document.gradients ?? [], marks.defs),\n    marks.body,\n    \"</svg>\",\n  ].join(\"\");\n};\n"
    },
    {
      "path": "packages/data/src/columns/divider.tsx",
      "target": "@ui/gpuixui/data/columns/divider.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { EventPayload } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { primaryButton, spokenText } from \"../../primitives/core/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Option from \"effect/Option\";\nimport { useRef } from \"react\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { TABLE_COLUMN_DIVIDER } from \"../table/recipe.js\";\nimport { controlFocusRing, useTheme } from \"../theme.js\";\nimport {\n  COLUMN_KEY_STEP,\n  clampWidth,\n  isColumnResizeKey,\n  readableWidth,\n  resizeWidthByKey,\n  widthBounds,\n} from \"./logic.js\";\n\ninterface ResizePress {\n  readonly x: number;\n  readonly width: number;\n}\n\nexport const DEFAULT_RESIZE_LABEL = \"Resize column\";\n\nexport interface TableColumnDividerProps {\n  readonly width: number;\n\n  readonly minWidth?: number;\n\n  readonly maxWidth?: number;\n\n  readonly onResize: (width: number) => void;\n\n  readonly label: string;\n\n  readonly step?: number;\n\n  readonly className?: string;\n  readonly testId?: string;\n}\n\nexport type TableColumnResizeProps = Omit<TableColumnDividerProps, \"width\">;\n\nexport const TableColumnDivider = ({\n  width,\n  minWidth,\n  maxWidth,\n  onResize,\n  label,\n  step = COLUMN_KEY_STEP,\n  className,\n  testId,\n}: TableColumnDividerProps) => {\n  const theme = useTheme();\n  const bounds = widthBounds(minWidth, maxWidth);\n\n  const track = clampWidth(readableWidth(width, bounds), bounds);\n  const name = spokenText(Option.fromNullishOr(label), DEFAULT_RESIZE_LABEL);\n  const press = useRef<Option.Option<ResizePress>>(Option.none());\n\n  return (\n    <div\n      testId={testId}\n      role=\"separator\"\n      aria-label={name}\n      aria-valuetext={`${track} pixels`}\n      tabIndex={0}\n      style={mergeStyles(\n        recipeStyle(cn(TABLE_COLUMN_DIVIDER, className), theme),\n        {\n          cursor: \"col-resize\",\n          ...controlFocusRing(theme),\n        },\n      )}\n      onMouseDown={(event: EventPayload) => {\n        if (!primaryButton(event)) return;\n        press.current = Option.some({ x: event.x ?? 0, width: track });\n      }}\n      onMouseMove={(event: EventPayload) =>\n        Option.match(press.current, {\n          onNone: () => {},\n          onSome: (start) => {\n            if (event.pressedButton !== 0) {\n              press.current = Option.none();\n              return;\n            }\n            onResize(\n              clampWidth(start.width + (event.x ?? 0) - start.x, bounds),\n            );\n          },\n        })\n      }\n      onMouseUp={() => {\n        press.current = Option.none();\n      }}\n      onKeyDown={(event: EventPayload) => {\n        if (isColumnResizeKey(event.key)) {\n          onResize(resizeWidthByKey(track, event.key, bounds, step));\n        }\n      }}\n    />\n  );\n};\n"
    },
    {
      "path": "packages/data/src/columns/logic.ts",
      "target": "@ui/gpuixui/data/columns/logic.ts",
      "type": "registry:lib",
      "content": "import { Match, pipe } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as HashMap from \"effect/HashMap\";\nimport * as Number from \"effect/Number\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport * as R from \"effect/Record\";\nimport * as Result from \"effect/Result\";\nimport * as Schema from \"effect/Schema\";\n\nexport const DEFAULT_MIN_COLUMN_WIDTH = 80;\n\nexport const DEFAULT_MAX_COLUMN_WIDTH = 640;\n\nexport const COLUMN_KEY_STEP = 16;\n\nexport interface ColumnDef {\n  readonly id: string;\n\n  readonly label: string;\n\n  readonly width: number;\n\n  readonly minWidth?: number;\n\n  readonly maxWidth?: number;\n}\n\nexport interface Column extends ColumnDef {\n  readonly visible: boolean;\n}\n\nexport interface ColumnState {\n  readonly columns: ReadonlyArray<Column>;\n}\n\nexport interface ColumnWidthBounds {\n  readonly minWidth: number;\n  readonly maxWidth: number;\n}\n\nconst readable = (value: number, fallback: number): number =>\n  globalThis.Number.isFinite(value) ? value : fallback;\n\nexport const widthBounds = (\n  minWidth: number = DEFAULT_MIN_COLUMN_WIDTH,\n  maxWidth: number = DEFAULT_MAX_COLUMN_WIDTH,\n): ColumnWidthBounds => {\n  const min = Math.max(1, readable(minWidth, DEFAULT_MIN_COLUMN_WIDTH));\n  return {\n    minWidth: min,\n    maxWidth: Math.max(min, readable(maxWidth, DEFAULT_MAX_COLUMN_WIDTH)),\n  };\n};\n\nexport const columnBounds = (column: ColumnDef): ColumnWidthBounds =>\n  widthBounds(column.minWidth, column.maxWidth);\n\nexport const clampWidth = (width: number, bounds: ColumnWidthBounds): number =>\n  Number.clamp({ minimum: bounds.minWidth, maximum: bounds.maxWidth })(width);\n\nexport const readableWidth = (\n  width: number,\n  bounds: ColumnWidthBounds,\n): number => (globalThis.Number.isNaN(width) ? bounds.minWidth : width);\n\nexport const clampColumnWidth = (width: number, column: ColumnDef): number =>\n  clampWidth(width, columnBounds(column));\n\nexport const initialColumns = (\n  defs: ReadonlyArray<ColumnDef>,\n): ColumnState => ({\n  columns: Arr.map(defs, (column) => ({\n    ...column,\n    width: clampColumnWidth(column.width, column),\n    visible: true,\n  })),\n});\n\nexport const columnResizeKeys = [\"left\", \"right\", \"home\", \"end\"] as const;\n\nexport const ColumnResizeKey = Schema.Literals(columnResizeKeys);\n\nexport type ColumnResizeKey = typeof ColumnResizeKey.Type;\n\nexport const isColumnResizeKey = Schema.is(ColumnResizeKey);\n\nexport const resizeWidthByKey = (\n  width: number,\n  key: ColumnResizeKey,\n  bounds: ColumnWidthBounds,\n  step: number = COLUMN_KEY_STEP,\n): number => {\n  const by = readable(step, COLUMN_KEY_STEP);\n\n  const from = readableWidth(width, bounds);\n  return pipe(\n    Match.value(key),\n    Match.when(\"left\", () => clampWidth(from - by, bounds)),\n    Match.when(\"right\", () => clampWidth(from + by, bounds)),\n    Match.when(\"home\", () => bounds.minWidth),\n    Match.when(\"end\", () => bounds.maxWidth),\n    Match.exhaustive,\n  );\n};\n\nexport const visibleColumns = (state: ColumnState): ReadonlyArray<Column> =>\n  Arr.filter(state.columns, (column) => column.visible);\n\nexport const hiddenColumns = (state: ColumnState): ReadonlyArray<string> =>\n  Arr.filterMap(state.columns, (column) =>\n    column.visible ? Result.failVoid : Result.succeed(column.id),\n  );\n\nexport const findColumn = (\n  state: ColumnState,\n  id: string,\n): Option.Option<Column> =>\n  Arr.findFirst(state.columns, (column) => column.id === id);\n\nexport const columnWidth = (\n  state: ColumnState,\n  id: string,\n): Option.Option<number> =>\n  Option.map(findColumn(state, id), (column) => column.width);\n\nexport const isColumnVisible = (state: ColumnState, id: string): boolean =>\n  pipe(\n    findColumn(state, id),\n    Option.match({ onNone: () => false, onSome: (column) => column.visible }),\n  );\n\nexport const contentWidth = (state: ColumnState): number =>\n  Arr.reduce(visibleColumns(state), 0, (total, column) => total + column.width);\n\nexport const columnOffset = (\n  state: ColumnState,\n  id: string,\n): Option.Option<number> =>\n  pipe(\n    visibleColumns(state),\n    Arr.findFirstIndex((column) => column.id === id),\n    Option.map((at) =>\n      Arr.reduce(\n        Arr.take(visibleColumns(state), at),\n        0,\n        (total, column) => total + column.width,\n      ),\n    ),\n  );\n\nconst withColumn = (\n  state: ColumnState,\n  id: string,\n  update: (column: Column) => Column,\n): ColumnState => ({\n  columns: Arr.map(state.columns, (column) =>\n    column.id === id ? update(column) : column,\n  ),\n});\n\nexport const setColumnVisible = (\n  state: ColumnState,\n  id: string,\n  visible: boolean,\n): ColumnState => {\n  const target = findColumn(state, id);\n  if (Option.isNone(target)) return state;\n  const current = target.value;\n  if (current.visible === visible) return state;\n  if (!visible && visibleColumns(state).length <= 1) return state;\n  return withColumn(state, id, (column) => ({ ...column, visible }));\n};\n\nexport const toggleColumn = (state: ColumnState, id: string): ColumnState =>\n  pipe(\n    findColumn(state, id),\n    Option.match({\n      onNone: () => state,\n      onSome: (column) => setColumnVisible(state, id, !column.visible),\n    }),\n  );\n\nexport const moveColumn = (\n  state: ColumnState,\n  id: string,\n  delta: number,\n): ColumnState => {\n  const visible = visibleColumns(state);\n  const fromVisible = Arr.findFirstIndex(visible, (column) => column.id === id);\n  if (Option.isNone(fromVisible)) return state;\n  const toVisible = Math.max(\n    0,\n    Math.min(visible.length - 1, fromVisible.value + delta),\n  );\n  if (toVisible === fromVisible.value) return state;\n  const nextVisible = Option.flatMap(\n    Arr.get(visible, fromVisible.value),\n    (column) =>\n      Arr.insertAt(Arr.remove(visible, fromVisible.value), toVisible, column),\n  );\n  if (Option.isNone(nextVisible)) return state;\n  const visibleSlots = Arr.reduce(\n    state.columns,\n    Arr.empty<number>(),\n    (slots, column, index) =>\n      column.visible ? Arr.append(slots, index) : slots,\n  );\n  return {\n    columns: Arr.map(state.columns, (column, index) =>\n      Option.match(\n        Option.flatMap(\n          Arr.findFirstIndex(visibleSlots, (slot) => slot === index),\n          (rank) => Arr.get(nextVisible.value, rank),\n        ),\n        { onNone: () => column, onSome: (next) => next },\n      ),\n    ),\n  };\n};\n\nexport const resizeColumn = (\n  state: ColumnState,\n  id: string,\n  width: number,\n): ColumnState => {\n  if (globalThis.Number.isNaN(width)) return state;\n  const target = findColumn(state, id);\n  if (Option.isNone(target)) return state;\n  const next = clampColumnWidth(width, target.value);\n  if (next === target.value.width) return state;\n  return withColumn(state, id, (column) => ({ ...column, width: next }));\n};\n\nexport const resetColumns = (defs: ReadonlyArray<ColumnDef>): ColumnState =>\n  initialColumns(defs);\n\nexport interface ColumnConfig {\n  readonly order: ReadonlyArray<string>;\n\n  readonly hidden: ReadonlyArray<string>;\n\n  readonly widths: Readonly<Record<string, number>>;\n}\n\nexport const columnConfig = (state: ColumnState): ColumnConfig => ({\n  order: Arr.map(state.columns, (column) => column.id),\n  hidden: hiddenColumns(state),\n  widths: R.fromEntries(\n    Arr.map(state.columns, (column) => [column.id, column.width] as const),\n  ),\n});\n\nconst StoredColumnConfig = Schema.Record(Schema.String, Schema.Unknown);\n\nconst decodeStoredColumnConfig = Schema.decodeUnknownOption(StoredColumnConfig);\n\nconst storedField = (stored: unknown, key: string): unknown =>\n  Option.match(decodeStoredColumnConfig(stored), {\n    onNone: () => undefined,\n    onSome: (record) => Option.getOrUndefined(R.get(record, key)),\n  });\n\nconst storedIds = (stored: unknown): ReadonlyArray<string> =>\n  Arr.isArray(stored) ? Arr.filter(stored, P.isString) : [];\n\nconst storedWidth = (stored: unknown, fallback: number): number =>\n  P.isNumber(stored) && globalThis.Number.isFinite(stored) ? stored : fallback;\n\nexport const restoreColumns = (\n  defs: ReadonlyArray<ColumnDef>,\n  stored: unknown,\n): ColumnState => {\n  const byId = HashMap.fromIterable(\n    Arr.map(defs, (column) => [column.id, column] as const),\n  );\n  const savedOrder = Arr.dedupe(\n    Arr.filter(storedIds(storedField(stored, \"order\")), (id) =>\n      HashMap.has(byId, id),\n    ),\n  );\n  const rest = Arr.filter(\n    Arr.map(defs, (column) => column.id),\n    (id) => !Arr.contains(savedOrder, id),\n  );\n  const widths = Option.getOrElse(\n    decodeStoredColumnConfig(storedField(stored, \"widths\")),\n    () => ({}),\n  );\n  const ordered = Arr.getSomes(\n    Arr.map(Arr.appendAll(savedOrder, rest), (id) => HashMap.get(byId, id)),\n  );\n  const withWidths: ColumnState = {\n    columns: Arr.map(ordered, (column) => ({\n      ...column,\n      width: clampColumnWidth(\n        storedWidth(\n          Option.getOrUndefined(R.get(widths, column.id)),\n          column.width,\n        ),\n        column,\n      ),\n      visible: true,\n    })),\n  };\n  return Arr.reduce(\n    storedIds(storedField(stored, \"hidden\")),\n    withWidths,\n    (state, id) => setColumnVisible(state, id, false),\n  );\n};\n"
    },
    {
      "path": "packages/data/src/columns/menu.tsx",
      "target": "@ui/gpuixui/data/columns/menu.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from \"../../components/dropdown-menu.js\";\nimport { spokenText } from \"../../primitives/core/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { columnName } from \"../sort/logic.js\";\nimport {\n  moveColumn,\n  setColumnVisible,\n  visibleColumns,\n  type Column,\n  type ColumnState,\n} from \"./logic.js\";\n\nexport const DEFAULT_COLUMN_MENU_LABEL = \"Columns\";\n\nexport interface ColumnMenuProps {\n  readonly state: ColumnState;\n\n  readonly onChange: (next: ColumnState) => void;\n\n  readonly onReset?: () => void;\n\n  readonly label?: string;\n\n  readonly contentLabel?: string;\n  readonly disabled?: boolean;\n\n  readonly testId?: string;\n}\n\nconst VisibilityRow = ({\n  column,\n  state,\n  onChange,\n  canHide,\n  testId,\n}: {\n  readonly column: Column;\n  readonly state: ColumnState;\n  readonly onChange: (next: ColumnState) => void;\n  readonly canHide: boolean;\n  readonly testId?: string;\n}) => (\n  <DropdownMenuCheckboxItem\n    checked={column.visible}\n    disabled={column.visible && !canHide}\n    onCheckedChange={(visible) =>\n      onChange(setColumnVisible(state, column.id, visible))\n    }\n    testId={testId === undefined ? undefined : `${testId}-toggle-${column.id}`}\n  >\n    {columnName(column.label, column.id)}\n  </DropdownMenuCheckboxItem>\n);\n\nconst MoveRow = ({\n  column,\n  at,\n  count,\n  state,\n  onChange,\n  testId,\n}: {\n  readonly column: Column;\n  readonly at: number;\n  readonly count: number;\n  readonly state: ColumnState;\n  readonly onChange: (next: ColumnState) => void;\n  readonly testId?: string;\n}) => (\n  <DropdownMenuSub>\n    <DropdownMenuSubTrigger\n      testId={testId === undefined ? undefined : `${testId}-move-${column.id}`}\n    >\n      {`Move ${columnName(column.label, column.id)}`}\n    </DropdownMenuSubTrigger>\n    <DropdownMenuSubContent\n      label={`Move ${columnName(column.label, column.id)}`}\n    >\n      <DropdownMenuItem\n        disabled={at === 0}\n        onSelect={() => onChange(moveColumn(state, column.id, -1))}\n        testId={\n          testId === undefined ? undefined : `${testId}-move-${column.id}-left`\n        }\n      >\n        Left\n      </DropdownMenuItem>\n      <DropdownMenuItem\n        disabled={at === count - 1}\n        onSelect={() => onChange(moveColumn(state, column.id, 1))}\n        testId={\n          testId === undefined ? undefined : `${testId}-move-${column.id}-right`\n        }\n      >\n        Right\n      </DropdownMenuItem>\n    </DropdownMenuSubContent>\n  </DropdownMenuSub>\n);\n\nexport const ColumnMenu = ({\n  state,\n  onChange,\n  onReset,\n  label,\n  contentLabel,\n  disabled,\n  testId,\n}: ColumnMenuProps) => {\n  const visible = visibleColumns(state);\n  const canHide = visible.length > 1;\n\n  const menuLabel = spokenText(\n    Option.fromNullishOr(label),\n    DEFAULT_COLUMN_MENU_LABEL,\n  );\n  const surfaceLabel = spokenText(\n    Option.fromNullishOr(contentLabel),\n    menuLabel,\n  );\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        variant=\"secondary\"\n        size=\"sm\"\n        disabled={disabled}\n        testId={testId}\n      >\n        {menuLabel}\n      </DropdownMenuTrigger>\n      <DropdownMenuContent label={surfaceLabel}>\n        {Arr.map(state.columns, (column) => (\n          <VisibilityRow\n            key={`visible-${column.id}`}\n            column={column}\n            state={state}\n            onChange={onChange}\n            canHide={canHide}\n            testId={testId}\n          />\n        ))}\n        <DropdownMenuSeparator />\n        {Arr.map(visible, (column, at) => (\n          <MoveRow\n            key={`move-${column.id}`}\n            column={column}\n            at={at}\n            count={visible.length}\n            state={state}\n            onChange={onChange}\n            testId={testId}\n          />\n        ))}\n        {Option.match(Option.fromNullishOr(onReset), {\n          onNone: () => null,\n          onSome: (reset) => (\n            <>\n              <DropdownMenuSeparator />\n              <DropdownMenuItem\n                onSelect={reset}\n                testId={testId === undefined ? undefined : `${testId}-reset`}\n              >\n                Reset columns\n              </DropdownMenuItem>\n            </>\n          ),\n        })}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/columns/use-columns.ts",
      "target": "@ui/gpuixui/data/columns/use-columns.ts",
      "type": "registry:lib",
      "content": "import type * as Option from \"effect/Option\";\nimport { useMemo, useState } from \"react\";\nimport {\n  columnOffset,\n  columnWidth,\n  contentWidth,\n  hiddenColumns,\n  initialColumns,\n  isColumnVisible,\n  moveColumn,\n  resizeColumn,\n  restoreColumns,\n  setColumnVisible,\n  toggleColumn,\n  visibleColumns,\n  type Column,\n  type ColumnDef,\n  type ColumnState,\n} from \"./logic.js\";\n\nexport interface UseColumnsOptions {\n  readonly columns: ReadonlyArray<ColumnDef>;\n\n  readonly stored?: unknown;\n}\n\nexport interface ColumnController {\n  readonly state: ColumnState;\n\n  readonly visible: ReadonlyArray<Column>;\n\n  readonly hidden: ReadonlyArray<string>;\n\n  readonly contentWidth: number;\n\n  readonly widthOf: (id: string) => Option.Option<number>;\n\n  readonly offsetOf: (id: string) => Option.Option<number>;\n  readonly isVisible: (id: string) => boolean;\n\n  readonly apply: (next: ColumnState) => void;\n  readonly setVisible: (id: string, visible: boolean) => void;\n  readonly toggle: (id: string) => void;\n  readonly move: (id: string, delta: number) => void;\n  readonly resize: (id: string, width: number) => void;\n  readonly reset: () => void;\n}\n\nexport const useColumns = ({\n  columns,\n  stored,\n}: UseColumnsOptions): ColumnController => {\n  const [state, setState] = useState<ColumnState>(() =>\n    restoreColumns(columns, stored),\n  );\n  const hidden = useMemo(() => hiddenColumns(state), [state]);\n\n  return {\n    state,\n    visible: visibleColumns(state),\n    hidden,\n    contentWidth: contentWidth(state),\n    widthOf: (id) => columnWidth(state, id),\n    offsetOf: (id) => columnOffset(state, id),\n    isVisible: (id) => isColumnVisible(state, id),\n    apply: setState,\n    setVisible: (id, visible) =>\n      setState((current) => setColumnVisible(current, id, visible)),\n    toggle: (id) => setState((current) => toggleColumn(current, id)),\n    move: (id, delta) => setState((current) => moveColumn(current, id, delta)),\n    resize: (id, width) =>\n      setState((current) => resizeColumn(current, id, width)),\n    reset: () => setState(initialColumns(columns)),\n  };\n};\n"
    },
    {
      "path": "packages/data/src/filter/field.tsx",
      "target": "@ui/gpuixui/data/filter/field.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport { Input } from \"../../primitives/index.js\";\nimport { spaceValue, spokenText, visibleCopy } from \"../../primitives/core/index.js\";\nimport * as Option from \"effect/Option\";\nimport { useTheme } from \"../theme.js\";\n\nexport interface TableFilterFieldProps {\n  readonly value: string;\n\n  readonly onValueChange: (next: string) => void;\n\n  readonly label?: string;\n\n  readonly placeholder?: string;\n\n  readonly width?: number;\n  readonly testId?: string;\n}\n\nexport const DEFAULT_FILTER_LABEL = \"Filter rows\";\n\nexport const DEFAULT_FILTER_PLACEHOLDER = \"Filter…\";\n\nexport const TableFilterField = ({\n  value,\n  onValueChange,\n  label,\n  placeholder,\n  width,\n  testId,\n}: TableFilterFieldProps) => {\n  const theme = useTheme();\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        flexDirection: \"row\",\n        alignItems: \"center\",\n        gap: spaceValue(theme, \"sm\"),\n        ...(width === undefined ? {} : { width }),\n      }}\n    >\n      <Input\n        label={spokenText(Option.fromNullishOr(label), DEFAULT_FILTER_LABEL)}\n        placeholder={visibleCopy(placeholder) ?? DEFAULT_FILTER_PLACEHOLDER}\n        value={value}\n        onValueChange={onValueChange}\n        size=\"sm\"\n        fullWidth\n        testId={testId}\n      />\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/grid-core.ts",
      "target": "@ui/gpuixui/data/grid-core.ts",
      "type": "registry:lib",
      "content": "import {\n  columnFilteringFeature,\n  constructTable,\n  createFilteredRowModel,\n  createPaginatedRowModel,\n  createSortedRowModel,\n  filterFn_includesString,\n  functionalUpdate,\n  globalFilteringFeature,\n  rowPaginationFeature,\n  rowSelectionFeature,\n  rowSortingFeature,\n  sortFn_basic,\n  tableFeatures,\n  type ColumnDef,\n  type PaginationState,\n  type RowData,\n  type SortingState,\n  type Table,\n} from \"@tanstack/table-core\";\nimport { storeReactivityBindings } from \"@tanstack/table-core/store-reactivity-bindings\";\nimport * as Arr from \"effect/Array\";\nimport * as HashSet from \"effect/HashSet\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport * as R from \"effect/Record\";\nimport * as Result from \"effect/Result\";\nimport * as Schema from \"effect/Schema\";\nimport { rowsOf } from \"./internal/list.js\";\nimport { readPagination } from \"./pagination/logic.js\";\n\nexport const gridFeatures = tableFeatures({\n  coreReactivityFeature: storeReactivityBindings(),\n  columnFilteringFeature,\n  globalFilteringFeature,\n  rowPaginationFeature,\n  rowSelectionFeature,\n  rowSortingFeature,\n  filteredRowModel: createFilteredRowModel(),\n  sortedRowModel: createSortedRowModel(),\n  paginatedRowModel: createPaginatedRowModel(),\n  filterFns: { includesString: filterFn_includesString },\n  sortFns: { basic: sortFn_basic },\n});\n\nexport type GridTable<Row extends RowData> = Table<typeof gridFeatures, Row>;\n\nexport interface GridColumn<Row> {\n  readonly id: string;\n  readonly valueOf: (row: Row) => string | number;\n}\n\nexport interface GridState {\n  readonly sorting: SortingState;\n  readonly globalFilter: string;\n  readonly rowSelection: Readonly<Record<string, true>>;\n\n  readonly pagination?: PaginationState;\n}\n\nexport type RestoredGridState = GridState & {\n  readonly pagination: PaginationState;\n};\n\nclass StoredSort extends Schema.Class<StoredSort>(\"StoredSort\")({\n  id: Schema.String,\n  desc: Schema.optionalKey(Schema.Boolean),\n}) {}\n\nconst decodeStoredSort = Schema.decodeUnknownOption(StoredSort);\n\nconst StoredRecord = Schema.Record(Schema.String, Schema.Unknown);\n\nconst decodeStoredRecord = Schema.decodeUnknownOption(StoredRecord);\n\nconst storedRecord = (stored: unknown): Record<string, unknown> =>\n  Option.getOrElse(decodeStoredRecord(stored), () =>\n    R.empty<string, unknown>(),\n  );\n\nconst storedSlice = (record: Record<string, unknown>, key: string): unknown =>\n  Option.getOrUndefined(R.get(record, key));\n\nconst storedSorting = (stored: unknown): SortingState =>\n  Arr.isArray(stored)\n    ? Arr.map(\n        Arr.getSomes(Arr.map(stored, (entry) => decodeStoredSort(entry))),\n        (entry) => ({\n          id: entry.id,\n          desc: entry.desc === true,\n        }),\n      )\n    : [];\n\nconst storedQuery = (stored: unknown): string =>\n  P.isString(stored) ? stored : \"\";\n\nconst storedChosen = (stored: unknown): Readonly<Record<string, true>> =>\n  Option.match(decodeStoredRecord(stored), {\n    onNone: () => ({}),\n    onSome: (record) =>\n      R.fromEntries(\n        Arr.filterMap(R.toEntries(record), ([key, value]) =>\n          value === true\n            ? Result.succeed([key, true] as const)\n            : Result.failVoid,\n        ),\n      ),\n  });\n\nexport const readGridState = (stored: unknown): RestoredGridState => {\n  const record = storedRecord(stored);\n  return {\n    sorting: storedSorting(storedSlice(record, \"sorting\")),\n    globalFilter: storedQuery(storedSlice(record, \"globalFilter\")),\n    rowSelection: storedChosen(storedSlice(record, \"rowSelection\")),\n    pagination: readPagination(storedSlice(record, \"pagination\")),\n  };\n};\n\nconst storedIds = (stored: unknown): ReadonlyArray<string> =>\n  Arr.isArray(stored) ? Arr.filter(stored, P.isString) : [];\n\nexport const retainSorting = <Row>(\n  sorting: SortingState,\n  columns: ReadonlyArray<GridColumn<Row>>,\n): SortingState =>\n  Arr.filter(sorting, (entry) =>\n    Arr.some(columns, (column) => column.id === entry.id),\n  );\n\nexport interface GridConfig {\n  readonly sorting: SortingState;\n  readonly globalFilter: string;\n  readonly rowSelection: Readonly<Record<string, true>>;\n  readonly pagination: PaginationState;\n}\n\nexport const gridConfig = (state: GridState): GridConfig => {\n  const current = readGridState(state);\n  return {\n    sorting: Arr.map(current.sorting, (entry) => ({\n      id: entry.id,\n      desc: entry.desc,\n    })),\n    globalFilter: current.globalFilter,\n    rowSelection: R.map(current.rowSelection, () => true as const),\n    pagination: {\n      pageIndex: current.pagination.pageIndex,\n      pageSize: current.pagination.pageSize,\n    },\n  };\n};\n\nexport interface GridChange {\n  readonly onSortingChange: (next: SortingState) => void;\n  readonly onGlobalFilterChange: (next: string) => void;\n  readonly onRowSelectionChange: (next: Readonly<Record<string, true>>) => void;\n\n  readonly onPaginationChange?: (next: PaginationState) => void;\n}\n\nexport interface ConstructGridOptions<Row> extends GridChange {\n  readonly rows: ReadonlyArray<Row>;\n  readonly keyOf: (row: Row) => string;\n  readonly columns: ReadonlyArray<GridColumn<Row>>;\n\n  readonly hiddenColumns?: ReadonlyArray<string>;\n\n  readonly state: GridState;\n}\n\nconst columnDefs = <Row extends RowData>(\n  columns: ReadonlyArray<GridColumn<Row>>,\n  hidden: HashSet.HashSet<string>,\n): ReadonlyArray<ColumnDef<typeof gridFeatures, Row>> =>\n  columns.map((column) => ({\n    id: column.id,\n    accessorFn: column.valueOf,\n    sortFn: \"basic\" as const,\n    enableGlobalFilter: !HashSet.has(hidden, column.id),\n  }));\n\nexport const constructGrid = <Row extends RowData>({\n  rows,\n  keyOf,\n  columns,\n  hiddenColumns,\n  state,\n  onSortingChange,\n  onGlobalFilterChange,\n  onRowSelectionChange,\n  onPaginationChange,\n}: ConstructGridOptions<Row>): GridTable<Row> => {\n  const current = readGridState(state);\n  return constructTable({\n    features: gridFeatures,\n    columns: columnDefs(\n      columns,\n      HashSet.fromIterable(storedIds(hiddenColumns)),\n    ),\n    data: rowsOf(rows, keyOf),\n    getRowId: keyOf,\n\n    state: {\n      sorting: [...retainSorting(current.sorting, columns)],\n      globalFilter: current.globalFilter.trim(),\n      rowSelection: current.rowSelection,\n      pagination: current.pagination,\n    },\n    autoResetPageIndex: false,\n    globalFilterFn: \"includesString\",\n    onSortingChange: (updater) =>\n      onSortingChange(functionalUpdate(updater, current.sorting)),\n    onGlobalFilterChange: (updater) =>\n      onGlobalFilterChange(functionalUpdate(updater, current.globalFilter)),\n    onRowSelectionChange: (updater) =>\n      onRowSelectionChange(functionalUpdate(updater, current.rowSelection)),\n    onPaginationChange: (updater) =>\n      onPaginationChange?.(functionalUpdate(updater, current.pagination)),\n  });\n};\n"
    },
    {
      "path": "packages/data/src/index.ts",
      "target": "@ui/gpuixui/data/index.ts",
      "type": "registry:lib",
      "content": "export {\n  DEFAULT_EMPTY_MESSAGE,\n  Table,\n  TableBody,\n  TableCaption,\n  TableCell,\n  TableEmpty,\n  TableHeader,\n  TableHeaderCell,\n  TableRow,\n  TableSkeleton,\n  TableViewport,\n  emptyMessage,\n} from \"./table/table.js\";\nexport type {\n  CellAlign,\n  CellLayoutProps,\n  TableBodyProps,\n  TableCaptionProps,\n  TableCellProps,\n  TableEmptyProps,\n  TableHeaderCellProps,\n  TableHeaderProps,\n  TableProps,\n  TableRowProps,\n  TableSkeletonProps,\n  TableViewportProps,\n} from \"./table/table.js\";\n\nexport {\n  DEFAULT_EXPANDER_WIDTH,\n  TableDetailRow,\n  TableExpandableRow,\n} from \"./table/expandable.js\";\nexport type {\n  TableDetailRowProps,\n  TableExpandableRowProps,\n} from \"./table/expandable.js\";\n\nexport { FrozenTable } from \"./table/frozen.js\";\nexport type { FrozenTableProps } from \"./table/frozen.js\";\n\nexport {\n  AreaChart,\n  BarChart,\n  CandleChart,\n  CHART_AXIS_WIDTH,\n  ChartLegend,\n  ComposedChart,\n  DEFAULT_CHART_HEIGHT,\n  DEFAULT_CHART_WIDTH,\n  DonutChart,\n  FunnelChart,\n  LineChart,\n  PieChart,\n  RadarChart,\n  RadialChart,\n  ScatterChart,\n  Sparkline,\n  StackedBar,\n} from \"./chart/chart.js\";\nexport type {\n  AreaChartProps,\n  BarChartProps,\n  CandleChartProps,\n  ChartDatum,\n  ChartLegendItem,\n  ChartLegendProps,\n  ChartProps,\n  ComposedChartProps,\n  DonutChartProps,\n  FunnelChartProps,\n  LineChartProps,\n  PieChartProps,\n  RadarChartProps,\n  RadialChartProps,\n  ScatterChartProps,\n  SparklineProps,\n  StackedBarProps,\n} from \"./chart/chart.js\";\n\nexport {\n  RadialBarChart,\n  SankeyChart,\n  SunburstChart,\n  TreemapChart,\n  type RadialBarChartProps,\n  type SankeyChartProps,\n  type SunburstChartProps,\n  type TreemapChartProps,\n} from \"./chart/structure.js\";\nexport {\n  flowGraph,\n  hierarchyLeaves,\n  hierarchyRoots,\n  hierarchyRows,\n  nameOf,\n  type FlowLink,\n  type FlowNode,\n  type HierarchyNode,\n  type HierarchyRow,\n} from \"./chart/hierarchy.js\";\n\nexport {\n  ANIMATION_FRAME_MS,\n  DEFAULT_ANIMATION_DURATION,\n  easeOutCubic,\n  interpolateSeries,\n  interpolateValues,\n} from \"./chart/animation.js\";\n\nexport { PointTooltip, paletteOf, seriesColourOf } from \"./chart/frame.js\";\nexport type {\n  ChartFrameProps,\n  LegendShape,\n  PointZone,\n  LegendSide,\n  TooltipIndicator,\n  PaintContext,\n  PaintLayer,\n  ValueLabel,\n} from \"./chart/frame.js\";\n\nexport {\n  areaBands,\n  areaPath,\n  barMarksInRange,\n  chartRange,\n  chartView,\n  chartWindow,\n  clipArea,\n  circlePath,\n  curvePath,\n  finite,\n  fullRing,\n  horizontalBarMarks,\n  niceBound,\n  pointMarks,\n  polygonPath,\n  radarCircleGrid,\n  radarGrid,\n  radarPoints,\n  radarSpokes,\n  radialArcs,\n  rangeTicks,\n  ringArc,\n  ringSegments,\n  scaleX,\n  scaleY,\n  seriesPoints,\n  valueLabel,\n  zeroY,\n} from \"./chart/geometry.js\";\nexport type { ChartRange, Curve, Point } from \"./chart/geometry.js\";\n\nexport {\n  chartScene,\n  sceneDefinition,\n  scenePointAt,\n  sceneRows,\n  sceneSvg,\n} from \"./chart/scene.js\";\nexport type {\n  SceneChartKind,\n  SceneChartOptions,\n  SceneRow,\n} from \"./chart/scene.js\";\n\nexport { chartDocument, layerMarks } from \"./chart/paint.js\";\nexport type { LayerPaint } from \"./chart/paint.js\";\n\nexport { chartSvg, defsMarkup, gradientMarkup } from \"./chart/svg.js\";\nexport type { ChartDocument, ChartGradient } from \"./chart/svg.js\";\n\nexport {\n  flatMark,\n  lineStroke,\n  markMarkup,\n  num,\n  solidFill,\n} from \"./chart/mark.js\";\nexport type {\n  CircleMark,\n  EllipseMark,\n  MarkFill,\n  MarkStroke,\n  PathMark,\n  RectMark,\n} from \"./chart/mark.js\";\n\nexport {\n  categoriesFromData,\n  configColour,\n  configColours,\n  configEntryOf,\n  configEntries,\n  configIcon,\n  configKeys,\n  configLabel,\n  configOf,\n  datumNumber,\n  seriesFromConfig,\n} from \"./chart/config.js\";\nexport type {\n  ChartConfig,\n  ChartConfigEntry,\n  ChartMode,\n  ChartRow,\n} from \"./chart/config.js\";\n\nexport {\n  AXIS_STEP_LIMIT,\n  axisTicks,\n  barMarks,\n  barSlot,\n  chartBox,\n  chartMarkup,\n  chartMaxOf,\n  chartScale,\n  donutMarks,\n  donutSegments,\n  drawableValue,\n  groupedBarMarks,\n  linePath,\n  niceCeiling,\n  seriesColour,\n  seriesDescription,\n  seriesMax,\n  seriesName,\n  stackedBarMarks,\n  stackedScale,\n  tickLabel,\n} from \"./chart/logic.js\";\nexport type {\n  BarSlot,\n  ChartBox,\n  ChartMark,\n  ChartSeries,\n} from \"./chart/logic.js\";\n\nexport { TableSortHeader, sortGlyph } from \"./sort/header.js\";\nexport type { TableSortHeaderProps } from \"./sort/header.js\";\nexport {\n  columnName,\n  directionIn,\n  directionOf,\n  pressSortOrder,\n  sortLabel,\n  sortOrderGlyph,\n  sortOrderLabel,\n  sortPosition,\n  toggleSort,\n} from \"./sort/logic.js\";\nexport type {\n  SortDirection,\n  SortOrder,\n  SortState,\n  SortValue,\n} from \"./sort/logic.js\";\n\nexport {\n  allSelected,\n  clearSelection,\n  currentKey,\n  emptySelection,\n  isSelected,\n  moveSelection,\n  neighbourKey,\n  pressMode,\n  pressSelection,\n  retainSelection,\n  selectAll,\n  selectionSize,\n  someSelected,\n} from \"./selection/logic.js\";\nexport type {\n  PressMode,\n  PressModifiers,\n  SelectionState,\n} from \"./selection/logic.js\";\nexport {\n  DEFAULT_SELECTION_CELL_LABEL,\n  DEFAULT_SELECTION_LABEL,\n  SELECTION_COLUMN_WIDTH,\n  TableSelectionCell,\n  TableSelectionHeader,\n} from \"./selection/column.js\";\nexport type {\n  TableSelectionCellProps,\n  TableSelectionHeaderProps,\n} from \"./selection/column.js\";\n\nexport {\n  DEFAULT_FILTER_LABEL,\n  DEFAULT_FILTER_PLACEHOLDER,\n  TableFilterField,\n} from \"./filter/field.js\";\nexport type { TableFilterFieldProps } from \"./filter/field.js\";\n\nexport {\n  constructGrid,\n  gridConfig,\n  gridFeatures,\n  readGridState,\n  retainSorting,\n} from \"./grid-core.js\";\nexport type {\n  ConstructGridOptions,\n  GridChange,\n  GridColumn,\n  GridConfig,\n  GridState,\n  GridTable,\n  RestoredGridState,\n} from \"./grid-core.js\";\n\nexport {\n  ALL_ROWS,\n  clampPageIndex,\n  DEFAULT_PAGINATION,\n  pageCountOf,\n  pageIndexOf,\n  pageSizeOf,\n  readPagination,\n  withPageSize,\n} from \"./pagination/logic.js\";\n\nexport { fieldValueOf, useTable } from \"./use-table.js\";\nexport type { TableController, UseTableOptions } from \"./use-table.js\";\n\nexport { ColumnMenu, DEFAULT_COLUMN_MENU_LABEL } from \"./columns/menu.js\";\nexport type { ColumnMenuProps } from \"./columns/menu.js\";\nexport { DEFAULT_RESIZE_LABEL, TableColumnDivider } from \"./columns/divider.js\";\nexport type {\n  TableColumnDividerProps,\n  TableColumnResizeProps,\n} from \"./columns/divider.js\";\nexport {\n  COLUMN_KEY_STEP,\n  ColumnResizeKey,\n  DEFAULT_MAX_COLUMN_WIDTH,\n  DEFAULT_MIN_COLUMN_WIDTH,\n  clampColumnWidth,\n  clampWidth,\n  columnBounds,\n  columnConfig,\n  columnOffset,\n  columnResizeKeys,\n  columnWidth,\n  contentWidth,\n  findColumn,\n  hiddenColumns,\n  initialColumns,\n  isColumnResizeKey,\n  isColumnVisible,\n  moveColumn,\n  readableWidth,\n  resetColumns,\n  resizeColumn,\n  resizeWidthByKey,\n  restoreColumns,\n  setColumnVisible,\n  toggleColumn,\n  visibleColumns,\n  widthBounds,\n} from \"./columns/logic.js\";\nexport type {\n  Column,\n  ColumnConfig,\n  ColumnDef,\n  ColumnState,\n  ColumnWidthBounds,\n} from \"./columns/logic.js\";\nexport { useColumns } from \"./columns/use-columns.js\";\nexport type {\n  ColumnController,\n  UseColumnsOptions,\n} from \"./columns/use-columns.js\";\n\nexport { VirtualList, visibleRange } from \"./virtual/list.js\";\nexport type { VirtualListProps, VisibleRange } from \"./virtual/list.js\";\n\nexport {\n  DEFAULT_OVERDRAW,\n  DEFAULT_ROW_HEIGHT,\n  anchorOffset,\n  createWindowCalculator,\n  rowHeightOf,\n  rowIndex,\n} from \"./virtual/window.js\";\nexport type {\n  VirtualWindow,\n  WindowCalculator,\n  WindowSpec,\n} from \"./virtual/window.js\";\n\nexport { useVirtualRows } from \"./virtual/use-virtual-rows.js\";\nexport type {\n  UseVirtualRowsOptions,\n  VirtualReveal,\n  VirtualRowsController,\n} from \"./virtual/use-virtual-rows.js\";\n\nexport { VirtualTable, VirtualTableBody } from \"./virtual/table.js\";\nexport type {\n  VirtualTableBodyProps,\n  VirtualTableProps,\n} from \"./virtual/table.js\";\n\nexport { VirtualTree } from \"./virtual/tree.js\";\nexport type { VirtualTreeProps } from \"./virtual/tree.js\";\n\nexport { KeyValue, DEFAULT_LABEL_WIDTH } from \"./surfaces/key-value.js\";\nexport type { KeyValueEntry, KeyValueProps } from \"./surfaces/key-value.js\";\n\nexport { StatCard, trendTone } from \"./surfaces/stat-card.js\";\nexport type { StatCardProps, StatTrend } from \"./surfaces/stat-card.js\";\n\nexport { PropertyPanel, isEditable } from \"./surfaces/property-panel.js\";\nexport type {\n  EditablePropertyRow,\n  PropertyPanelProps,\n  PropertyRow,\n  ReadonlyPropertyRow,\n} from \"./surfaces/property-panel.js\";\n\nexport {\n  DataToolbar,\n  DEFAULT_SEARCH_DEBOUNCE_MS,\n  DEFAULT_SEARCH_LABEL,\n} from \"./surfaces/toolbar.js\";\nexport type { DataToolbarProps, ToolbarSearch } from \"./surfaces/toolbar.js\";\n\nexport {\n  AdvancedFilter,\n  comparisonLabels,\n  DEFAULT_FILTER_TITLE,\n  groupLabels,\n} from \"./surfaces/advanced-filter.js\";\nexport type {\n  AdvancedFilterField,\n  AdvancedFilterProps,\n} from \"./surfaces/advanced-filter.js\";\n\nexport {\n  appendAt,\n  appendClause,\n  clauseAt,\n  clauseCount,\n  clauseFieldName,\n  comparisons,\n  describeTree,\n  emptyTree,\n  FilterTree,\n  filterByTree,\n  isAll,\n  isAny,\n  isComparison,\n  isFilterTree,\n  isNot,\n  isPredicate,\n  matchesComparison,\n  matchesTree,\n  negateAt,\n  propertyName,\n  readFilterTree,\n  removeAt,\n  setGroupKind,\n  UNNAMED_CLAUSE_FIELD,\n  UNREADABLE_TREE,\n  updateAt,\n  updatePredicate,\n} from \"./surfaces/logic.js\";\nexport type {\n  AllClause,\n  AnyClause,\n  Comparison,\n  FieldPredicate,\n  FilterPath,\n  GroupKind,\n  NotClause,\n} from \"./surfaces/logic.js\";\n\nexport {\n  TREE_INDENT,\n  TreeKey,\n  branchKeys,\n  collapseAll,\n  cursorRow,\n  emptyCursor,\n  expandAll,\n  expandTo,\n  expandedPath,\n  expandedSet,\n  findRow,\n  flattenTree,\n  isExpanded,\n  isTreeKey,\n  paintedRows,\n  setExpanded,\n  toggleExpanded,\n  treeCursorMove,\n  treeKeys,\n  treeName,\n  typeAheadRow,\n} from \"./tree/logic.js\";\nexport type {\n  PaintedRow,\n  TreeCursor,\n  TreeRow,\n  TreeSpec,\n} from \"./tree/logic.js\";\n\nexport { TREE_INSET, TreeView } from \"./tree/tree-view.js\";\nexport type { TreeViewProps } from \"./tree/tree-view.js\";\n\nexport { useExpandedRows } from \"./tree/use-expanded.js\";\nexport type {\n  ExpandedRowsController,\n  UseExpandedRowsOptions,\n} from \"./tree/use-expanded.js\";\n"
    },
    {
      "path": "packages/data/src/internal/list.ts",
      "target": "@ui/gpuixui/data/internal/list.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as P from \"effect/Predicate\";\nimport * as Str from \"effect/String\";\n\nexport const listOf = <A>(\n  value: ReadonlyArray<A> | null | undefined,\n): ReadonlyArray<A> => (Arr.isArray(value) ? value : []);\n\nexport const recordsOf = <A extends { readonly key: string }>(\n  value: ReadonlyArray<A> | undefined,\n): ReadonlyArray<A> =>\n  Arr.filter(\n    listOf(value),\n    (entry) => P.isObject(entry) && P.isString(entry.key),\n  );\n\nexport const textsOf = (\n  value: ReadonlyArray<string> | undefined,\n): ReadonlyArray<string> =>\n  Arr.map(listOf(value), (entry) => (P.isString(entry) ? entry : \"\"));\n\nconst namesRow = (value: unknown): boolean =>\n  P.isString(value) && Str.isNonEmpty(Str.trim(value));\n\nexport const rowsOf = <A>(\n  value: ReadonlyArray<A> | null | undefined,\n  keyOf: (row: A) => string,\n): ReadonlyArray<A> =>\n  Arr.filter(listOf(value), (row) => P.isObject(row) && namesRow(keyOf(row)));\n"
    },
    {
      "path": "packages/data/src/internal/painted.ts",
      "target": "@ui/gpuixui/data/internal/painted.ts",
      "type": "registry:lib",
      "content": "import type { EventPayload, StyleDesc } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\nimport { useCallback, useState } from \"react\";\nimport { paintableSize } from \"./size.js\";\n\nexport interface PaintedHeight {\n  readonly measured: Option.Option<number>;\n\n  readonly onLayout: (event: EventPayload) => void;\n}\n\nexport const usePaintedHeight = (\n  onLayout: ((event: EventPayload) => void) | undefined,\n): PaintedHeight => {\n  const [measured, setMeasured] = useState<Option.Option<number>>(\n    Option.none(),\n  );\n  const report = useCallback(\n    (event: EventPayload) => {\n      onLayout?.(event);\n      Option.match(paintableSize(event.height), {\n        onNone: () => {},\n        onSome: (height) =>\n          setMeasured((current) =>\n            Option.contains(current, height) ? current : Option.some(height),\n          ),\n      });\n    },\n    [onLayout],\n  );\n  return { measured, onLayout: report };\n};\n\nexport const paneFill: (height: Option.Option<number>) => StyleDesc =\n  Option.match({\n    onNone: () => ({\n      display: \"flex\",\n      flexDirection: \"column\",\n      flexGrow: 1,\n      flexShrink: 1,\n      flexBasis: 0,\n      minHeight: 0,\n    }),\n    onSome: () => ({}),\n  });\n"
    },
    {
      "path": "packages/data/src/internal/readable.tsx",
      "target": "@ui/gpuixui/data/internal/readable.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { StyleDesc } from \"@gpuix/react\";\nimport * as P from \"effect/Predicate\";\nimport type { ReactNode } from \"react\";\n\nexport interface ReadableProps {\n  readonly testId?: string;\n  readonly style?: StyleDesc;\n}\n\nexport const readable = (\n  value: ReactNode,\n  { testId, style }: ReadableProps = {},\n) =>\n  P.isString(value) || P.isNumber(value) ? (\n    <text testId={testId} style={style}>\n      {value}\n    </text>\n  ) : (\n    <div testId={testId} style={style}>\n      {value}\n    </div>\n  );\n"
    },
    {
      "path": "packages/data/src/internal/size.ts",
      "target": "@ui/gpuixui/data/internal/size.ts",
      "type": "registry:lib",
      "content": "import * as Option from \"effect/Option\";\n\nexport const paintableSize = (\n  size: number | undefined,\n): Option.Option<number> =>\n  Option.filter(\n    Option.fromNullishOr(size),\n    (value) => Number.isFinite(value) && value >= 0,\n  );\n"
    },
    {
      "path": "packages/data/src/pagination/logic.ts",
      "target": "@ui/gpuixui/data/pagination/logic.ts",
      "type": "registry:lib",
      "content": "import type { PaginationState } from \"@tanstack/table-core\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport * as R from \"effect/Record\";\nimport * as Schema from \"effect/Schema\";\n\nexport const ALL_ROWS = Number.POSITIVE_INFINITY;\n\nexport const DEFAULT_PAGINATION: PaginationState = {\n  pageIndex: 0,\n  pageSize: ALL_ROWS,\n};\n\nconst StoredPage = Schema.Record(Schema.String, Schema.Unknown);\n\nconst decodeStoredPage = Schema.decodeUnknownOption(StoredPage);\n\nconst storedField = (stored: unknown, key: string): unknown =>\n  Option.match(decodeStoredPage(stored), {\n    onNone: () => undefined,\n    onSome: (record) => Option.getOrUndefined(R.get(record, key)),\n  });\n\nexport const pageIndexOf = (value: unknown): number =>\n  P.isNumber(value) && Number.isFinite(value)\n    ? Math.max(0, Math.floor(value))\n    : DEFAULT_PAGINATION.pageIndex;\n\nexport const pageSizeOf = (value: unknown): number =>\n  P.isNumber(value) && Number.isFinite(value) && value >= 1\n    ? Math.floor(value)\n    : ALL_ROWS;\n\nexport const pageCountOf = (rowCount: number, pageSize: number): number => {\n  const rows =\n    Number.isFinite(rowCount) && rowCount > 0 ? Math.floor(rowCount) : 0;\n  const size = pageSizeOf(pageSize);\n  return size === ALL_ROWS ? 1 : Math.max(1, Math.ceil(rows / size));\n};\n\nexport const clampPageIndex = (\n  pageIndex: number,\n  rowCount: number,\n  pageSize: number,\n): number =>\n  Math.min(pageIndexOf(pageIndex), pageCountOf(rowCount, pageSize) - 1);\n\nexport const withPageSize = (\n  current: PaginationState,\n  pageSize: unknown,\n): PaginationState => {\n  const size = pageSizeOf(pageSize);\n  const top =\n    pageSizeOf(current.pageSize) === ALL_ROWS\n      ? 0\n      : pageIndexOf(current.pageIndex) * pageSizeOf(current.pageSize);\n  return {\n    pageIndex: size === ALL_ROWS ? 0 : Math.floor(top / size),\n    pageSize: size,\n  };\n};\n\nexport const readPagination = (stored: unknown): PaginationState => ({\n  pageIndex: pageIndexOf(storedField(stored, \"pageIndex\")),\n  pageSize: pageSizeOf(storedField(stored, \"pageSize\")),\n});\n"
    },
    {
      "path": "packages/data/src/recipe.ts",
      "target": "@ui/gpuixui/data/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(256);\n"
    },
    {
      "path": "packages/data/src/selection/column.tsx",
      "target": "@ui/gpuixui/data/selection/column.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { StyleDesc } from \"@gpuix/react\";\nimport { Checkbox } from \"../../primitives/index.js\";\nimport { spokenText } from \"../../primitives/core/index.js\";\nimport * as Option from \"effect/Option\";\nimport { TableCell, TableHeaderCell } from \"../table/table.js\";\nimport { themeColors, useTheme, type Theme } from \"../theme.js\";\n\nexport const SELECTION_COLUMN_WIDTH = 40;\n\nexport const DEFAULT_SELECTION_LABEL = \"Select every row\";\n\nexport const DEFAULT_SELECTION_CELL_LABEL = \"Select this row\";\n\nconst selectionBox = (\n  theme: Theme,\n  fill: string | undefined,\n  interactive: boolean,\n): StyleDesc => {\n  const colors = themeColors(theme);\n  return {\n    display: \"flex\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    backgroundColor: fill,\n    ...(interactive ? { hover: { backgroundColor: colors.accent } } : {}),\n  };\n};\n\nexport interface TableSelectionHeaderProps {\n  readonly checked: boolean;\n\n  readonly indeterminate?: boolean;\n\n  readonly onCheckedChange: (selected: boolean) => void;\n\n  readonly label?: string;\n\n  readonly width?: number;\n  readonly disabled?: boolean;\n\n  readonly testId?: string;\n}\n\nexport const TableSelectionHeader = ({\n  checked,\n  indeterminate = false,\n  onCheckedChange,\n  label,\n  width = SELECTION_COLUMN_WIDTH,\n  disabled = false,\n  testId,\n}: TableSelectionHeaderProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n\n  const boxLabel = spokenText(\n    Option.fromNullishOr(label),\n    DEFAULT_SELECTION_LABEL,\n  );\n  return (\n    <TableHeaderCell\n      width={width}\n      align=\"center\"\n      testId={testId === undefined ? undefined : `${testId}-cell`}\n    >\n      <div\n        testId={testId === undefined ? undefined : `${testId}-target`}\n        style={selectionBox(theme, colors.card, false)}\n      >\n        <Checkbox\n          checked={checked}\n          indeterminate={indeterminate}\n          onCheckedChange={onCheckedChange}\n          label={boxLabel}\n          disabled={disabled}\n          size=\"sm\"\n          testId={testId}\n        />\n      </div>\n    </TableHeaderCell>\n  );\n};\n\nexport interface TableSelectionCellProps {\n  readonly checked: boolean;\n\n  readonly onCheckedChange: (selected: boolean) => void;\n\n  readonly label: string;\n\n  readonly width?: number;\n  readonly disabled?: boolean;\n\n  readonly testId?: string;\n}\n\nexport const TableSelectionCell = ({\n  checked,\n  onCheckedChange,\n  label,\n  width = SELECTION_COLUMN_WIDTH,\n  disabled = false,\n  testId,\n}: TableSelectionCellProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const boxLabel = spokenText(\n    Option.fromNullishOr(label),\n    DEFAULT_SELECTION_CELL_LABEL,\n  );\n  return (\n    <TableCell\n      width={width}\n      align=\"center\"\n      testId={testId === undefined ? undefined : `${testId}-cell`}\n    >\n      <div\n        testId={testId === undefined ? undefined : `${testId}-target`}\n        style={selectionBox(\n          theme,\n          checked ? colors.selected : colors.card,\n          true,\n        )}\n      >\n        <Checkbox\n          checked={checked}\n          onCheckedChange={onCheckedChange}\n          label={boxLabel}\n          disabled={disabled}\n          size=\"sm\"\n          testId={testId}\n        />\n      </div>\n    </TableCell>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/selection/logic.ts",
      "target": "@ui/gpuixui/data/selection/logic.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as HashSet from \"effect/HashSet\";\nimport * as Option from \"effect/Option\";\n\nexport type PressMode = \"replace\" | \"toggle\" | \"range\";\n\nexport interface PressModifiers {\n  readonly shift?: boolean;\n  readonly ctrl?: boolean;\n  readonly cmd?: boolean;\n}\n\nexport interface SelectionState {\n  readonly selected: HashSet.HashSet<string>;\n\n  readonly anchor: Option.Option<string>;\n}\n\nexport const emptySelection: SelectionState = {\n  selected: HashSet.empty(),\n  anchor: Option.none(),\n};\n\nexport const pressMode = (modifiers: PressModifiers = {}): PressMode =>\n  modifiers.shift === true\n    ? \"range\"\n    : modifiers.cmd === true || modifiers.ctrl === true\n      ? \"toggle\"\n      : \"replace\";\n\nexport const isSelected = (state: SelectionState, key: string): boolean =>\n  HashSet.has(state.selected, key);\n\nexport const selectionSize = (state: SelectionState): number =>\n  HashSet.size(state.selected);\n\nexport const allSelected = (\n  state: SelectionState,\n  visible: ReadonlyArray<string>,\n): boolean =>\n  visible.length > 0 &&\n  Arr.every(visible, (key) => HashSet.has(state.selected, key));\n\nexport const someSelected = (\n  state: SelectionState,\n  visible: ReadonlyArray<string>,\n): boolean =>\n  !allSelected(state, visible) &&\n  Arr.some(visible, (key) => HashSet.has(state.selected, key));\n\nconst indexIn = (\n  order: ReadonlyArray<string>,\n  key: string,\n): Option.Option<number> =>\n  Arr.findFirstIndex(order, (candidate) => candidate === key);\n\nexport const pressSelection = (\n  state: SelectionState,\n  key: string,\n  mode: PressMode,\n  order: ReadonlyArray<string>,\n): SelectionState => {\n  if (mode === \"toggle\") {\n    const selected = HashSet.has(state.selected, key)\n      ? HashSet.remove(state.selected, key)\n      : HashSet.add(state.selected, key);\n    return { selected, anchor: Option.some(key) };\n  }\n  if (mode === \"range\") {\n    const from = Option.flatMap(state.anchor, (anchor) =>\n      indexIn(order, anchor),\n    );\n    const to = indexIn(order, key);\n    if (Option.isNone(from) || Option.isNone(to)) {\n      return { selected: HashSet.make(key), anchor: Option.some(key) };\n    }\n    const start = Math.min(from.value, to.value);\n    const count = Math.abs(from.value - to.value) + 1;\n    const spanned = Arr.take(Arr.drop(order, start), count);\n    return {\n      selected: Arr.reduce(spanned, state.selected, (selected, key) =>\n        HashSet.add(selected, key),\n      ),\n\n      anchor: state.anchor,\n    };\n  }\n  return { selected: HashSet.make(key), anchor: Option.some(key) };\n};\n\nexport const selectAll = (\n  _state: SelectionState,\n  order: ReadonlyArray<string>,\n): SelectionState => ({\n  selected: HashSet.fromIterable(order),\n  anchor: Option.none(),\n});\n\nexport const clearSelection = (_state: SelectionState): SelectionState =>\n  emptySelection;\n\nexport const retainSelection = (\n  state: SelectionState,\n  visible: ReadonlyArray<string>,\n): SelectionState => {\n  const shown = HashSet.fromIterable(visible);\n  return {\n    selected: HashSet.filter(state.selected, (key) => HashSet.has(shown, key)),\n    anchor: Option.filter(state.anchor, (key) => HashSet.has(shown, key)),\n  };\n};\n\nexport const currentKey = (state: SelectionState): Option.Option<string> =>\n  Option.filter(state.anchor, (key) => HashSet.has(state.selected, key));\n\nexport const neighbourKey = (\n  order: ReadonlyArray<string>,\n  current: Option.Option<string>,\n  delta: number,\n): Option.Option<string> =>\n  Option.match(\n    Option.flatMap(current, (key) => indexIn(order, key)),\n    {\n      onNone: () => Option.fromNullishOr(order[0]),\n      onSome: (index) =>\n        Option.fromNullishOr(\n          order[Math.max(0, Math.min(order.length - 1, index + delta))],\n        ),\n    },\n  );\n\nexport const moveSelection = (\n  state: SelectionState,\n  order: ReadonlyArray<string>,\n  delta: number,\n): SelectionState =>\n  Option.match(neighbourKey(order, currentKey(state), delta), {\n    onNone: () => state,\n    onSome: (key) => ({\n      selected: HashSet.make(key),\n      anchor: Option.some(key),\n    }),\n  });\n"
    },
    {
      "path": "packages/data/src/sort/header.tsx",
      "target": "@ui/gpuixui/data/sort/header.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { EventModifiers } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\nimport { Button } from \"../../primitives/button.js\";\nimport {\n  TableColumnDivider,\n  type TableColumnResizeProps,\n} from \"../columns/divider.js\";\nimport { textStyle, themeColors } from \"../theme.js\";\nimport type { CellAlign } from \"../table/table.js\";\nimport { TableHeaderCell } from \"../table/table.js\";\nimport { useTheme } from \"../theme.js\";\nimport {\n  columnName,\n  sortOrderGlyph,\n  sortOrderLabel,\n  toggleSort,\n  type SortDirection,\n  type SortOrder,\n  type SortState,\n} from \"./logic.js\";\n\ninterface TableSortHeaderShape {\n  readonly field: string;\n\n  readonly label: string;\n\n  readonly sort: Option.Option<SortState>;\n\n  readonly order?: SortOrder;\n\n  readonly initialDirection?: SortDirection;\n\n  readonly width?: number;\n\n  readonly align?: CellAlign;\n\n  readonly resize?: TableColumnResizeProps;\n  readonly testId?: string;\n}\n\nexport type TableSortHeaderProps = TableSortHeaderShape &\n  (\n    | {\n        readonly onSort: (next: SortState) => void;\n        readonly onSortPress?: undefined;\n      }\n    | {\n        readonly onSortPress: (\n          field: string,\n          initialDirection: SortDirection,\n          modifiers?: EventModifiers,\n        ) => void;\n        readonly onSort?: undefined;\n      }\n  );\n\nexport const sortGlyph = (\n  sort: Option.Option<SortState>,\n  field: string,\n): string => sortOrderGlyph(Option.toArray(sort), field);\n\nexport const TableSortHeader = (props: TableSortHeaderProps) => {\n  const {\n    field,\n    label,\n    sort,\n    order: orderProp,\n    initialDirection = \"asc\",\n    width,\n    align = \"left\",\n    resize,\n    testId,\n  } = props;\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const order = orderProp ?? Option.toArray(sort);\n  const glyph = sortOrderGlyph(order, field);\n\n  const name = columnName(label, field);\n  return (\n    <TableHeaderCell width={width} align={align} testId={testId}>\n      <Button\n        variant=\"ghost\"\n        size=\"sm\"\n        fullWidth\n        label={sortOrderLabel(label, order, field)}\n        style={{ paddingLeft: 0, paddingRight: 0 }}\n        testId={testId === undefined ? undefined : `${testId}-sort`}\n        onActivate={(event) => {\n          if (props.onSortPress !== undefined) {\n            props.onSortPress(field, initialDirection, event.modifiers);\n            return;\n          }\n          props.onSort(toggleSort(sort, field, initialDirection));\n        }}\n        trailing={\n          glyph.length === 0 ? undefined : (\n            <text\n              style={{\n                ...textStyle(theme, \"caption\"),\n                color: colors.mutedForeground,\n              }}\n            >\n              {glyph}\n            </text>\n          )\n        }\n      >\n        {name}\n      </Button>\n      {width === undefined || resize === undefined ? null : (\n        <TableColumnDivider width={width} {...resize} />\n      )}\n    </TableHeaderCell>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/sort/logic.ts",
      "target": "@ui/gpuixui/data/sort/logic.ts",
      "type": "registry:lib",
      "content": "import { spokenText } from \"../../primitives/core/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { pipe } from \"effect\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface SortState {\n  readonly field: string;\n  readonly direction: SortDirection;\n}\n\nexport type SortOrder = ReadonlyArray<SortState>;\n\nexport type SortValue = string | number;\n\nconst flip = (direction: SortDirection): SortDirection =>\n  direction === \"asc\" ? \"desc\" : \"asc\";\n\nexport const toggleSort = (\n  current: Option.Option<SortState>,\n  field: string,\n  initialDirection: SortDirection = \"asc\",\n): SortState =>\n  pipe(\n    current,\n    Option.filter((state) => state.field === field),\n    Option.match({\n      onNone: () => ({ field, direction: initialDirection }),\n      onSome: (state) => ({ field, direction: flip(state.direction) }),\n    }),\n  );\n\nexport const pressSortOrder = (\n  order: SortOrder,\n  field: string,\n  initialDirection: SortDirection = \"asc\",\n  additive: boolean = false,\n): SortOrder => {\n  const at = Arr.findFirstIndex(order, (entry) => entry.field === field);\n  if (additive) {\n    return Option.match(at, {\n      onNone: () => [...order, { field, direction: initialDirection }],\n      onSome: (index) =>\n        Arr.map(order, (entry, position) =>\n          position === index\n            ? { ...entry, direction: flip(entry.direction) }\n            : entry,\n        ),\n    });\n  }\n  return Option.match(Arr.head(order), {\n    onNone: () => [{ field, direction: initialDirection }],\n    onSome: (primary) => [\n      {\n        field,\n        direction:\n          primary.field === field ? flip(primary.direction) : initialDirection,\n      },\n    ],\n  });\n};\n\nexport const sortPosition = (\n  order: SortOrder,\n  field: string,\n): Option.Option<number> =>\n  Option.map(\n    Arr.findFirstIndex(order, (entry) => entry.field === field),\n    (index) => index + 1,\n  );\n\nexport const directionOf = (\n  sort: Option.Option<SortState>,\n  field: string,\n): Option.Option<SortDirection> =>\n  Option.flatMap(sort, (state) =>\n    state.field === field ? Option.some(state.direction) : Option.none(),\n  );\n\nexport const directionIn = (\n  order: SortOrder,\n  field: string,\n): Option.Option<SortDirection> =>\n  Option.map(\n    Arr.findFirst(order, (entry) => entry.field === field),\n    (entry) => entry.direction,\n  );\n\nexport const sortLabel = (\n  label: string,\n  sort: Option.Option<SortState>,\n  field: string,\n): string => sortOrderLabel(label, Option.toArray(sort), field);\n\nexport const columnName = (label: string, field: string): string =>\n  spokenText(Option.fromNullishOr(label), field);\n\nexport const sortOrderLabel = (\n  label: string,\n  order: SortOrder,\n  field: string,\n): string => {\n  const name = columnName(label, field);\n  return Option.match(\n    Option.all([directionIn(order, field), sortPosition(order, field)]),\n    {\n      onNone: () => `${name}, sortable`,\n      onSome: ([direction, position]) => {\n        const state = `${name}, sorted ${direction === \"asc\" ? \"ascending\" : \"descending\"}`;\n        return order.length > 1\n          ? `${state}, sort ${position} of ${order.length}`\n          : state;\n      },\n    },\n  );\n};\n\nexport const sortOrderGlyph = (order: SortOrder, field: string): string =>\n  Option.match(\n    Option.all([directionIn(order, field), sortPosition(order, field)]),\n    {\n      onNone: () => \"\",\n      onSome: ([direction, position]) => {\n        const arrow = direction === \"asc\" ? \"↑\" : \"↓\";\n        return order.length > 1 ? `${arrow}${position}` : arrow;\n      },\n    },\n  );\n"
    },
    {
      "path": "packages/data/src/surfaces/advanced-filter.tsx",
      "target": "@ui/gpuixui/data/surfaces/advanced-filter.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"../../components/dropdown-menu.js\";\nimport { Button, Input } from \"../../primitives/index.js\";\nimport { spokenText } from \"../../primitives/core/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport * as Str from \"effect/String\";\nimport type { ReactNode } from \"react\";\nimport { listOf } from \"../internal/list.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { useTheme } from \"../theme.js\";\nimport {\n  appendAt,\n  clauseFieldName,\n  comparisons,\n  describeTree,\n  emptyTree,\n  isFilterTree,\n  negateAt,\n  removeAt,\n  setGroupKind,\n  UNREADABLE_TREE,\n  updateAt,\n  withClauseField,\n  type Comparison,\n  type FieldPredicate,\n  type FilterPath,\n  type FilterTree,\n  type GroupKind,\n} from \"./logic.js\";\nimport {\n  ADVANCED_FILTER,\n  FILTER_CLAUSE,\n  FILTER_GROUP,\n  FILTER_SENTENCE,\n} from \"./recipe.js\";\n\nexport interface AdvancedFilterField {\n  readonly field: string;\n\n  readonly label: string;\n}\n\nexport interface AdvancedFilterProps extends Omit<\n  Props,\n  \"children\" | \"onChange\"\n> {\n  readonly fields: ReadonlyArray<AdvancedFilterField>;\n\n  readonly tree: FilterTree;\n\n  readonly onChange: (next: FilterTree) => void;\n\n  readonly title?: string;\n  readonly className?: string;\n\n  readonly testId?: string;\n}\n\nexport const comparisonLabels: Readonly<Record<Comparison, string>> = {\n  equals: \"equals\",\n  notEquals: \"does not equal\",\n  contains: \"contains\",\n  greaterThan: \"greater than\",\n  lessThan: \"less than\",\n};\n\nexport const DEFAULT_FILTER_TITLE = \"Filters\";\n\nexport const groupLabels: Readonly<Record<\"all\" | \"any\" | \"not\", string>> = {\n  all: \"all of\",\n  any: \"any of\",\n  not: \"not\",\n};\n\nconst labelOf = (\n  fields: ReadonlyArray<AdvancedFilterField>,\n  field: string,\n): string =>\n  Arr.findFirst(fields, (candidate) => candidate.field === field).pipe(\n    Option.map((candidate) =>\n      spokenText(\n        Option.fromNullishOr(candidate.label),\n        clauseFieldName(candidate.field),\n      ),\n    ),\n    Option.getOrElse(() => clauseFieldName(field)),\n  );\n\nconst slotOf = (path: FilterPath): string =>\n  path.length === 0 ? \"0\" : Arr.join(Arr.map(path, String), \"-\");\n\nconst clauseFor = (\n  fields: ReadonlyArray<AdvancedFilterField>,\n): FieldPredicate => ({\n  kind: \"predicate\",\n  field: Arr.head(fields).pipe(\n    Option.map((field) => field.field),\n    Option.getOrElse(() => \"\"),\n  ),\n  comparison: \"equals\",\n  value: \"\",\n});\n\nconst fieldList = (\n  fields: ReadonlyArray<AdvancedFilterField>,\n): ReadonlyArray<AdvancedFilterField> =>\n  Arr.filter(\n    listOf(fields),\n    (field): boolean =>\n      P.isObject(field) &&\n      P.isString(field.field) &&\n      Str.isNonEmpty(Str.trim(field.field)),\n  );\n\nconst ClauseRow = ({\n  root,\n  clause,\n  path,\n  slot,\n  fields,\n  removable,\n  onChange,\n  testId,\n}: {\n  readonly root: FilterTree;\n  readonly clause: FieldPredicate;\n  readonly path: FilterPath;\n\n  readonly slot: string;\n  readonly fields: ReadonlyArray<AdvancedFilterField>;\n\n  readonly removable: boolean;\n  readonly onChange: (next: FilterTree) => void;\n  readonly testId: string | undefined;\n}) => {\n  const theme = useTheme();\n  const id = testId === undefined ? undefined : `${testId}-${slot}`;\n  const replace = (change: Partial<FieldPredicate>): void => {\n    onChange(updateAt(root, path, { ...clause, ...change }));\n  };\n  return (\n    <div style={recipeStyle(FILTER_CLAUSE, theme)}>\n      <DropdownMenu>\n        <DropdownMenuTrigger\n          variant=\"secondary\"\n          size=\"sm\"\n          testId={id === undefined ? undefined : `${id}-field`}\n        >\n          {labelOf(fields, clause.field)}\n        </DropdownMenuTrigger>\n        <DropdownMenuContent label=\"Field\">\n          {Arr.map(fields, (field) => (\n            <DropdownMenuItem\n              key={field.field}\n              onSelect={() =>\n                onChange(\n                  updateAt(root, path, withClauseField(clause, field.field)),\n                )\n              }\n              testId={\n                id === undefined ? undefined : `${id}-field-${field.field}`\n              }\n            >\n              {labelOf(fields, field.field)}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuContent>\n      </DropdownMenu>\n      <DropdownMenu>\n        <DropdownMenuTrigger\n          variant=\"secondary\"\n          size=\"sm\"\n          testId={id === undefined ? undefined : `${id}-comparison`}\n        >\n          {comparisonLabels[clause.comparison]}\n        </DropdownMenuTrigger>\n        <DropdownMenuContent label=\"Comparison\">\n          {Arr.map(comparisons, (comparison) => (\n            <DropdownMenuItem\n              key={comparison}\n              onSelect={() => replace({ comparison })}\n              testId={\n                id === undefined ? undefined : `${id}-comparison-${comparison}`\n              }\n            >\n              {comparisonLabels[comparison]}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuContent>\n      </DropdownMenu>\n      <Input\n        label={`${labelOf(fields, clause.field)} value`}\n        value={clause.value}\n        onValueChange={(value) => replace({ value })}\n        size=\"sm\"\n        testId={id === undefined ? undefined : `${id}-value`}\n      />\n      {removable ? (\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          label=\"Remove clause\"\n          onActivate={() => onChange(removeAt(root, path))}\n          testId={id === undefined ? undefined : `${id}-remove`}\n        >\n          Remove\n        </Button>\n      ) : undefined}\n    </div>\n  );\n};\n\nconst Group = ({\n  root,\n  path,\n  kind,\n  children,\n  onAdd,\n  onRemove,\n  onChange,\n  slot,\n  testId,\n}: {\n  readonly root: FilterTree;\n\n  readonly path: FilterPath;\n  readonly kind: GroupKind;\n  readonly children: ReactNode;\n\n  readonly onAdd?: () => void;\n\n  readonly onRemove?: () => void;\n  readonly onChange: (next: FilterTree) => void;\n\n  readonly slot: string;\n  readonly testId: string | undefined;\n}) => {\n  const theme = useTheme();\n\n  const base =\n    testId === undefined\n      ? undefined\n      : path.length === 0\n        ? testId\n        : `${testId}-${slot}`;\n  return (\n    <div style={recipeStyle(FILTER_GROUP, theme)}>\n      <div style={recipeStyle(\"flex flex-row items-center gap-2\", theme)}>\n        <DropdownMenu>\n          <DropdownMenuTrigger\n            variant=\"ghost\"\n            size=\"sm\"\n            aria-description=\"Change how this group combines its clauses\"\n            testId={base === undefined ? undefined : `${base}-kind`}\n          >\n            {groupLabels[kind]}\n          </DropdownMenuTrigger>\n          <DropdownMenuContent label=\"Combine the group\">\n            <DropdownMenuItem\n              onSelect={() => onChange(setGroupKind(root, path, \"all\"))}\n              testId={base === undefined ? undefined : `${base}-kind-all`}\n            >\n              Match all\n            </DropdownMenuItem>\n            <DropdownMenuItem\n              onSelect={() => onChange(setGroupKind(root, path, \"any\"))}\n              testId={base === undefined ? undefined : `${base}-kind-any`}\n            >\n              Match any\n            </DropdownMenuItem>\n            <DropdownMenuItem\n              onSelect={() => onChange(negateAt(root, path))}\n              testId={base === undefined ? undefined : `${base}-kind-negate`}\n            >\n              {kind === \"not\" ? \"Do not negate\" : \"Negate\"}\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n        {Option.match(Option.fromNullishOr(onRemove), {\n          onNone: () => null,\n          onSome: (remove) => (\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              label=\"Remove group\"\n              onActivate={remove}\n              testId={\n                testId === undefined\n                  ? undefined\n                  : `${testId}-${slot}-group-remove`\n              }\n            >\n              Remove group\n            </Button>\n          ),\n        })}\n      </div>\n      {children}\n      {Option.match(Option.fromNullishOr(onAdd), {\n        onNone: () => null,\n        onSome: (add) => (\n          <Button\n            variant=\"ghost\"\n            size=\"sm\"\n            label={`Add a group to the ${groupLabels[kind]} group`}\n            onActivate={add}\n            testId={base === undefined ? undefined : `${base}-add-group`}\n          >\n            Add group\n          </Button>\n        ),\n      })}\n    </div>\n  );\n};\n\nconst Tree = ({\n  root,\n  node,\n  path,\n  fields,\n  onChange,\n  onAddClause,\n  onAddGroup,\n  testId,\n  removable,\n  canAdd,\n}: {\n  readonly root: FilterTree;\n\n  readonly node: FilterTree;\n  readonly path: FilterPath;\n  readonly fields: ReadonlyArray<AdvancedFilterField>;\n  readonly onChange: (next: FilterTree) => void;\n\n  readonly onAddClause: (path: FilterPath) => void;\n\n  readonly onAddGroup: (path: FilterPath) => void;\n  readonly testId: string | undefined;\n\n  readonly removable: boolean;\n\n  readonly canAdd: boolean;\n}) => {\n  if (node.kind === \"predicate\") {\n    return (\n      <ClauseRow\n        root={root}\n        clause={node}\n        path={path}\n        slot={slotOf(path)}\n        fields={fields}\n        removable={removable}\n        onChange={onChange}\n        testId={testId}\n      />\n    );\n  }\n  if (node.kind === \"not\") {\n    return (\n      <Group\n        root={root}\n        path={path}\n        kind=\"not\"\n        slot={slotOf(path)}\n        testId={testId}\n        onChange={onChange}\n        onRemove={removable ? () => onChange(removeAt(root, path)) : undefined}\n      >\n        <Tree\n          root={root}\n          node={node.child}\n          path={[...path, 0]}\n          fields={fields}\n          onChange={onChange}\n          onAddClause={onAddClause}\n          onAddGroup={onAddGroup}\n          testId={testId}\n          removable={false}\n          canAdd={canAdd}\n        />\n      </Group>\n    );\n  }\n  return (\n    <Group\n      root={root}\n      path={path}\n      kind={node.kind}\n      slot={slotOf(path)}\n      testId={testId}\n      onChange={onChange}\n      onRemove={removable ? () => onChange(removeAt(root, path)) : undefined}\n      onAdd={path.length === 0 || !canAdd ? undefined : () => onAddGroup(path)}\n    >\n      {Arr.map(node.children, (child, at) => (\n        <Tree\n          key={`${at}`}\n          root={root}\n          node={child}\n          path={[...path, at]}\n          fields={fields}\n          onChange={onChange}\n          onAddClause={onAddClause}\n          onAddGroup={onAddGroup}\n          testId={testId}\n          removable\n          canAdd={canAdd}\n        />\n      ))}\n      {path.length === 0 || !canAdd ? undefined : (\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          label={`Add a clause to the ${groupLabels[node.kind]} group`}\n          onActivate={() => onAddClause(path)}\n          testId={\n            testId === undefined ? undefined : `${testId}-${slotOf(path)}-add`\n          }\n        >\n          Add clause\n        </Button>\n      )}\n    </Group>\n  );\n};\n\nexport const AdvancedFilter = ({\n  fields,\n  tree,\n  onChange,\n  title,\n  className,\n  style,\n  testId,\n  ...rest\n}: AdvancedFilterProps) => {\n  const theme = useTheme();\n  const heading = spokenText(Option.fromNullishOr(title), DEFAULT_FILTER_TITLE);\n\n  const offered = fieldList(fields);\n\n  const shown = Option.match(Option.fromNullishOr(tree), {\n    onNone: () => Option.some(emptyTree),\n    onSome: (value) =>\n      isFilterTree(value) ? Option.some(value) : Option.none(),\n  });\n  const base = Option.getOrElse(shown, () => emptyTree);\n  const addClause = (path: FilterPath): void => {\n    onChange(appendAt(base, path, clauseFor(offered)));\n  };\n  const addGroup = (path: FilterPath): void => {\n    onChange(\n      appendAt(base, path, { kind: \"all\", children: [clauseFor(offered)] }),\n    );\n  };\n  return (\n    <div\n      {...rest}\n      style={mergeStyles(\n        recipeStyle(cn(ADVANCED_FILTER, className), theme),\n        style,\n      )}\n    >\n      <text\n        role=\"heading\"\n        aria-level={3}\n        style={recipeStyle(\"text-xs font-medium text-muted-foreground\", theme)}\n      >\n        {heading}\n      </text>\n      <text style={recipeStyle(FILTER_SENTENCE, theme)}>\n        {Option.match(shown, {\n          onNone: () => UNREADABLE_TREE,\n          onSome: describeTree,\n        })}\n      </text>\n      {Option.match(shown, {\n        onNone: () => null,\n        onSome: (readable) => (\n          <Tree\n            root={readable}\n            node={readable}\n            path={[]}\n            fields={offered}\n            onChange={onChange}\n            onAddClause={addClause}\n            onAddGroup={addGroup}\n            testId={testId}\n            removable={false}\n            canAdd={offered.length > 0}\n          />\n        ),\n      })}\n      <div style={recipeStyle(\"flex flex-row items-center gap-2\", theme)}>\n        {Option.isSome(shown) && offered.length > 0 ? (\n          <>\n            <Button\n              variant=\"secondary\"\n              size=\"sm\"\n              onActivate={() => addClause([])}\n              testId={testId === undefined ? undefined : `${testId}-add`}\n            >\n              Add clause\n            </Button>\n            <Button\n              variant=\"secondary\"\n              size=\"sm\"\n              onActivate={() => addGroup([])}\n              testId={testId === undefined ? undefined : `${testId}-add-group`}\n            >\n              Add group\n            </Button>\n          </>\n        ) : undefined}\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onActivate={() => onChange(emptyTree)}\n          testId={testId === undefined ? undefined : `${testId}-clear`}\n        >\n          Clear\n        </Button>\n      </div>\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/surfaces/key-value.tsx",
      "target": "@ui/gpuixui/data/surfaces/key-value.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { spaceValue } from \"../../primitives/core/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Arr from \"effect/Array\";\nimport type { ReactNode } from \"react\";\nimport { recordsOf } from \"../internal/list.js\";\nimport { readable } from \"../internal/readable.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { useTheme } from \"../theme.js\";\nimport { propertyName } from \"./logic.js\";\nimport { KEY_VALUE_LABEL, KEY_VALUE_ROW, KEY_VALUE_VALUE } from \"./recipe.js\";\n\nexport interface KeyValueEntry {\n  readonly key: string;\n\n  readonly label: string;\n\n  readonly value: ReactNode;\n}\n\nexport interface KeyValueProps extends Omit<Props, \"children\"> {\n  readonly entries: ReadonlyArray<KeyValueEntry>;\n\n  readonly labelWidth?: number;\n\n  readonly valueWidth?: number;\n  readonly className?: string;\n\n  readonly testId?: string;\n}\n\nexport const DEFAULT_LABEL_WIDTH = 120;\n\nexport const KeyValue = ({\n  entries,\n  labelWidth = DEFAULT_LABEL_WIDTH,\n  valueWidth,\n  className,\n  style,\n  testId,\n  ...rest\n}: KeyValueProps) => {\n  const theme = useTheme();\n  const labelTrack =\n    Number.isFinite(labelWidth) && labelWidth > 0\n      ? labelWidth\n      : DEFAULT_LABEL_WIDTH;\n  const valueTrack =\n    valueWidth !== undefined && Number.isFinite(valueWidth) && valueWidth > 0\n      ? valueWidth\n      : undefined;\n  return (\n    <div\n      {...rest}\n      role=\"list\"\n      style={mergeStyles(recipeStyle(cn(\"\", className), theme), style)}\n    >\n      {Arr.map(recordsOf(entries), (entry) => (\n        <div\n          key={entry.key}\n          role=\"listitem\"\n          style={{\n            display: \"flex\",\n            flexDirection: \"row\",\n            alignItems: \"center\",\n            gap: spaceValue(theme, \"sm\"),\n            ...recipeStyle(KEY_VALUE_ROW, theme),\n          }}\n        >\n          <text\n            testId={\n              testId === undefined ? undefined : `${testId}-label-${entry.key}`\n            }\n            style={{\n              ...recipeStyle(KEY_VALUE_LABEL, theme),\n              width: labelTrack,\n              flexGrow: 0,\n              flexShrink: 0,\n              flexBasis: labelTrack,\n            }}\n          >\n            {propertyName(entry.label, entry.key)}\n          </text>\n          {readable(entry.value, {\n            testId:\n              testId === undefined ? undefined : `${testId}-value-${entry.key}`,\n            style: {\n              ...recipeStyle(KEY_VALUE_VALUE, theme),\n              ...(valueTrack === undefined\n                ? { flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0 }\n                : {\n                    width: valueTrack,\n                    flexGrow: 0,\n                    flexShrink: 0,\n                    flexBasis: valueTrack,\n                  }),\n            },\n          })}\n        </div>\n      ))}\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/surfaces/logic.ts",
      "target": "@ui/gpuixui/data/surfaces/logic.ts",
      "type": "registry:lib",
      "content": "import { spokenText } from \"../../primitives/core/index.js\";\nimport { Match } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Number from \"effect/Number\";\nimport * as Option from \"effect/Option\";\nimport * as Schema from \"effect/Schema\";\nimport * as Str from \"effect/String\";\n\nexport const propertyName = (label: string, key: string): string =>\n  spokenText(Option.fromNullishOr(label), key);\n\nexport const UNNAMED_CLAUSE_FIELD = \"Field\";\n\nexport const clauseFieldName = (field: string): string =>\n  spokenText(Option.fromNullishOr(field), UNNAMED_CLAUSE_FIELD);\n\nexport const comparisons = [\n  \"equals\",\n  \"notEquals\",\n  \"contains\",\n  \"greaterThan\",\n  \"lessThan\",\n] as const;\n\nexport const Comparison = Schema.Literals(comparisons);\n\nexport type Comparison = typeof Comparison.Type;\n\nexport type GroupKind = \"all\" | \"any\" | \"not\";\n\nexport interface FieldPredicate {\n  readonly kind: \"predicate\";\n\n  readonly field: string;\n  readonly comparison: Comparison;\n\n  readonly value: string;\n}\n\nexport interface AllClause {\n  readonly kind: \"all\";\n  readonly children: ReadonlyArray<FilterTree>;\n}\n\nexport interface AnyClause {\n  readonly kind: \"any\";\n  readonly children: ReadonlyArray<FilterTree>;\n}\n\nexport interface NotClause {\n  readonly kind: \"not\";\n  readonly child: FilterTree;\n}\n\nexport type FilterTree = FieldPredicate | AllClause | AnyClause | NotClause;\n\nexport const FilterTree: Schema.Schema<FilterTree> = Schema.Union([\n  Schema.Struct({\n    kind: Schema.tag(\"predicate\"),\n    field: Schema.String,\n    comparison: Comparison,\n    value: Schema.String,\n  }),\n  Schema.Struct({\n    kind: Schema.tag(\"all\"),\n    children: Schema.Array(Schema.suspend(() => FilterTree)),\n  }),\n  Schema.Struct({\n    kind: Schema.tag(\"any\"),\n    children: Schema.Array(Schema.suspend(() => FilterTree)),\n  }),\n  Schema.Struct({\n    kind: Schema.tag(\"not\"),\n    child: Schema.suspend(() => FilterTree),\n  }),\n]);\n\nexport const UNREADABLE_TREE = \"a filter this release cannot read\";\n\nexport const isFilterTree = (value: unknown): value is FilterTree =>\n  Schema.is(FilterTree)(value);\n\nexport const isComparison = (value: unknown): value is Comparison =>\n  Schema.is(Comparison)(value);\n\nexport const readFilterTree = (value: unknown): Option.Option<FilterTree> =>\n  isFilterTree(value) ? Option.some(value) : Option.none();\n\nexport const isPredicate = (tree: FilterTree): tree is FieldPredicate =>\n  tree.kind === \"predicate\";\n\nexport const isAll = (tree: FilterTree): tree is AllClause =>\n  tree.kind === \"all\";\n\nexport const isAny = (tree: FilterTree): tree is AnyClause =>\n  tree.kind === \"any\";\n\nexport const isNot = (tree: FilterTree): tree is NotClause =>\n  tree.kind === \"not\";\n\nconst numeric = (value: string | number): Option.Option<number> =>\n  typeof value === \"number\" ? Option.some(value) : Number.parse(value);\n\nconst ordered = (\n  left: string | number,\n  right: string,\n  compare: (one: number, other: number) => boolean,\n): boolean =>\n  Option.match(Option.all([numeric(left), numeric(right)]), {\n    onNone: () => false,\n    onSome: ([one, other]) => compare(one, other),\n  });\n\nexport const matchesComparison = (\n  comparison: Comparison,\n  left: string | number,\n  right: string,\n): boolean =>\n  Arr.contains(comparisons, comparison) &&\n  Match.value(comparison).pipe(\n    Match.when(\"equals\", () => `${left}` === right),\n    Match.when(\"notEquals\", () => `${left}` !== right),\n    Match.when(\"contains\", () =>\n      Str.includes(Str.toLowerCase(right), 0)(Str.toLowerCase(`${left}`)),\n    ),\n    Match.when(\"greaterThan\", () =>\n      ordered(left, right, (one, other) => one > other),\n    ),\n    Match.when(\"lessThan\", () =>\n      ordered(left, right, (one, other) => one < other),\n    ),\n    Match.exhaustive,\n  );\n\nconst matchesReadable = <Row>(\n  tree: FilterTree,\n  row: Row,\n  valueOf: (row: Row, field: string) => string | number,\n): boolean =>\n  Match.value(tree).pipe(\n    Match.when(isPredicate, (clause) =>\n      matchesComparison(\n        clause.comparison,\n        valueOf(row, clause.field),\n        clause.value,\n      ),\n    ),\n    Match.when(isAll, (group) =>\n      Arr.every(group.children, (child) =>\n        matchesReadable(child, row, valueOf),\n      ),\n    ),\n    Match.when(isAny, (group) =>\n      Arr.some(group.children, (child) => matchesReadable(child, row, valueOf)),\n    ),\n    Match.when(isNot, (group) => !matchesReadable(group.child, row, valueOf)),\n    Match.exhaustive,\n  );\n\nexport const matchesTree = <Row>(\n  tree: FilterTree,\n  row: Row,\n  valueOf: (row: Row, field: string) => string | number,\n): boolean => isFilterTree(tree) && matchesReadable(tree, row, valueOf);\n\nexport const filterByTree = <Row>(\n  rows: ReadonlyArray<Row>,\n  tree: FilterTree,\n  valueOf: (row: Row, field: string) => string | number,\n): ReadonlyArray<Row> =>\n  isFilterTree(tree)\n    ? Arr.filter(rows, (row) => matchesReadable(tree, row, valueOf))\n    : [];\n\nconst clauseCountReadable = (tree: FilterTree): number =>\n  Match.value(tree).pipe(\n    Match.when(isPredicate, () => 1),\n    Match.when(isAll, (group) =>\n      Arr.reduce(\n        group.children,\n        0,\n        (total, child) => total + clauseCountReadable(child),\n      ),\n    ),\n    Match.when(isAny, (group) =>\n      Arr.reduce(\n        group.children,\n        0,\n        (total, child) => total + clauseCountReadable(child),\n      ),\n    ),\n    Match.when(isNot, (group) => clauseCountReadable(group.child)),\n    Match.exhaustive,\n  );\n\nexport const clauseCount = (tree: FilterTree): number =>\n  isFilterTree(tree) ? clauseCountReadable(tree) : 0;\n\nconst describeReadable = (tree: FilterTree): string =>\n  Match.value(tree).pipe(\n    Match.when(isPredicate, (clause) =>\n      Str.trim(\n        `${clauseFieldName(clause.field)} ${clause.comparison} ${clause.value}`,\n      ),\n    ),\n    Match.when(isAll, (group) =>\n      group.children.length === 0\n        ? \"everything\"\n        : `all of (${Arr.map(group.children, describeReadable).join(\", \")})`,\n    ),\n    Match.when(isAny, (group) =>\n      group.children.length === 0\n        ? \"nothing\"\n        : `any of (${Arr.map(group.children, describeReadable).join(\", \")})`,\n    ),\n    Match.when(isNot, (group) => `not (${describeReadable(group.child)})`),\n    Match.exhaustive,\n  );\n\nexport const describeTree = (tree: FilterTree): string =>\n  isFilterTree(tree) ? describeReadable(tree) : UNREADABLE_TREE;\n\nconst updatePredicateReadable = (\n  tree: FilterTree,\n  change: {\n    readonly field?: string;\n    readonly comparison?: Comparison;\n    readonly value?: string;\n  },\n): FilterTree =>\n  Match.value(tree).pipe(\n    Match.when(isPredicate, (clause) => ({\n      ...clause,\n      ...(change.field === undefined ? {} : { field: change.field }),\n      ...(change.comparison === undefined\n        ? {}\n        : { comparison: change.comparison }),\n      ...(change.value === undefined ? {} : { value: change.value }),\n    })),\n    Match.when(isAll, (group) => ({\n      ...group,\n      children: Arr.map(group.children, (child) =>\n        updatePredicateReadable(child, change),\n      ),\n    })),\n    Match.when(isAny, (group) => ({\n      ...group,\n      children: Arr.map(group.children, (child) =>\n        updatePredicateReadable(child, change),\n      ),\n    })),\n    Match.when(isNot, (group) => ({\n      ...group,\n      child: updatePredicateReadable(group.child, change),\n    })),\n    Match.exhaustive,\n  );\n\nexport const updatePredicate = (\n  tree: FilterTree,\n  change: {\n    readonly field?: string;\n    readonly comparison?: Comparison;\n    readonly value?: string;\n  },\n): FilterTree =>\n  isFilterTree(tree) ? updatePredicateReadable(tree, change) : tree;\n\nexport type FilterPath = ReadonlyArray<number>;\n\nexport const emptyTree: FilterTree = { kind: \"all\", children: [] };\n\nconst nodeAt = (\n  tree: FilterTree,\n  path: FilterPath,\n): Option.Option<FilterTree> => {\n  if (path.length === 0) return Option.some(tree);\n  const [head, ...rest] = path;\n  return Option.flatMap(Option.fromNullishOr(head), (at) =>\n    Match.value(tree).pipe(\n      Match.when(isPredicate, () => Option.none()),\n      Match.when(isAll, (group) => childAt(group.children, at, rest)),\n      Match.when(isAny, (group) => childAt(group.children, at, rest)),\n      Match.when(isNot, (group) =>\n        at === 0 ? nodeAt(group.child, rest) : Option.none(),\n      ),\n      Match.exhaustive,\n    ),\n  );\n};\n\nconst childAt = (\n  children: ReadonlyArray<FilterTree>,\n  at: number,\n  rest: FilterPath,\n): Option.Option<FilterTree> =>\n  Arr.get(children, at).pipe(Option.flatMap((child) => nodeAt(child, rest)));\n\nexport const clauseAt = (\n  tree: FilterTree,\n  path: FilterPath,\n): Option.Option<FieldPredicate> =>\n  isFilterTree(tree)\n    ? Option.flatMap(nodeAt(tree, path), (node) =>\n        isPredicate(node) ? Option.some(node) : Option.none(),\n      )\n    : Option.none();\n\nconst asGroup = (node: FilterTree, kind: \"all\" | \"any\"): FilterTree =>\n  Match.value(node).pipe(\n    Match.when(isPredicate, (clause) => ({ kind, children: [clause] })),\n    Match.when(isAll, (group) => ({ kind, children: group.children })),\n    Match.when(isAny, (group) => ({ kind, children: group.children })),\n    Match.when(isNot, (group) => asGroup(group.child, kind)),\n    Match.exhaustive,\n  );\n\nexport const withClauseField = (\n  clause: FieldPredicate,\n  field: string,\n): FieldPredicate => ({ ...clause, field, value: \"\" });\n\nexport const updateAt = (\n  tree: FilterTree,\n  path: FilterPath,\n  next: FilterTree,\n): FilterTree => {\n  if (!isFilterTree(tree) || !isFilterTree(next)) return tree;\n  if (path.length === 0) return next;\n  const [head, ...rest] = path;\n  const replaceChild = (children: ReadonlyArray<FilterTree>) =>\n    Arr.map(children, (child, at) =>\n      at === head ? updateAt(child, rest, next) : child,\n    );\n  return Match.value(tree).pipe(\n    Match.when(isPredicate, () => tree),\n    Match.when(isAll, (group) => ({\n      ...group,\n      children: replaceChild(group.children),\n    })),\n    Match.when(isAny, (group) => ({\n      ...group,\n      children: replaceChild(group.children),\n    })),\n    Match.when(isNot, (group) =>\n      head === 0\n        ? { ...group, child: updateAt(group.child, rest, next) }\n        : group,\n    ),\n    Match.exhaustive,\n  );\n};\n\nexport const setGroupKind = (\n  tree: FilterTree,\n  path: FilterPath,\n  kind: \"all\" | \"any\",\n): FilterTree => {\n  if (!isFilterTree(tree)) return tree;\n  return Option.match(nodeAt(tree, path), {\n    onNone: () => tree,\n    onSome: (node) => updateAt(tree, path, asGroup(node, kind)),\n  });\n};\n\nexport const negateAt = (tree: FilterTree, path: FilterPath): FilterTree => {\n  if (!isFilterTree(tree)) return tree;\n  return Option.match(nodeAt(tree, path), {\n    onNone: () => tree,\n    onSome: (node) =>\n      updateAt(\n        tree,\n        path,\n        isNot(node) ? node.child : { kind: \"not\", child: node },\n      ),\n  });\n};\n\nexport const removeAt = (tree: FilterTree, path: FilterPath): FilterTree => {\n  if (!isFilterTree(tree)) return tree;\n  if (path.length === 0) return tree;\n  const [head, ...rest] = path;\n  const dropChild = (children: ReadonlyArray<FilterTree>) =>\n    Arr.filter(children, (_, at) => at !== head);\n  return Match.value(tree).pipe(\n    Match.when(isPredicate, () => tree),\n    Match.when(isAll, (group) => ({\n      ...group,\n      children:\n        rest.length === 0\n          ? dropChild(group.children)\n          : Arr.map(group.children, (child, at) =>\n              at === head ? removeAt(child, rest) : child,\n            ),\n    })),\n    Match.when(isAny, (group) => ({\n      ...group,\n      children:\n        rest.length === 0\n          ? dropChild(group.children)\n          : Arr.map(group.children, (child, at) =>\n              at === head ? removeAt(child, rest) : child,\n            ),\n    })),\n    Match.when(isNot, (group) =>\n      head === 0 && rest.length > 0\n        ? { ...group, child: removeAt(group.child, rest) }\n        : tree,\n    ),\n    Match.exhaustive,\n  );\n};\n\nconst appendNode = (tree: FilterTree, node: FilterTree): FilterTree =>\n  Match.value(tree).pipe(\n    Match.when(isPredicate, () => ({\n      kind: \"all\" as const,\n      children: [tree, node],\n    })),\n    Match.when(isAll, (group) => ({\n      ...group,\n      children: [...group.children, node],\n    })),\n    Match.when(isAny, (group) => ({\n      ...group,\n      children: [...group.children, node],\n    })),\n    Match.when(isNot, (group) => ({\n      ...group,\n      child: appendNode(group.child, node),\n    })),\n    Match.exhaustive,\n  );\n\nexport const appendClause = (\n  tree: FilterTree,\n  clause: FieldPredicate,\n): FilterTree =>\n  isFilterTree(tree) && isFilterTree(clause) ? appendNode(tree, clause) : tree;\n\nexport const appendAt = (\n  tree: FilterTree,\n  path: FilterPath,\n  added: FilterTree,\n): FilterTree => {\n  if (!isFilterTree(tree) || !isFilterTree(added)) return tree;\n\n  if (path.length === 0) return appendNode(tree, added);\n  const appendInto = (current: FilterTree, rest: FilterPath): FilterTree => {\n    if (rest.length === 0) {\n      return isAll(current) || isAny(current)\n        ? appendNode(current, added)\n        : current;\n    }\n    const [head, ...tail] = rest;\n    const intoChildren = (\n      children: ReadonlyArray<FilterTree>,\n    ): ReadonlyArray<FilterTree> =>\n      Arr.map(children, (child, at) =>\n        at === head ? appendInto(child, tail) : child,\n      );\n    return Match.value(current).pipe(\n      Match.when(isPredicate, () => current),\n      Match.when(isNot, (group) =>\n        head === 0\n          ? { ...group, child: appendInto(group.child, tail) }\n          : current,\n      ),\n      Match.when(isAll, (group) => ({\n        ...group,\n        children: intoChildren(group.children),\n      })),\n      Match.when(isAny, (group) => ({\n        ...group,\n        children: intoChildren(group.children),\n      })),\n      Match.exhaustive,\n    );\n  };\n  return appendInto(tree, path);\n};\n"
    },
    {
      "path": "packages/data/src/surfaces/property-panel.tsx",
      "target": "@ui/gpuixui/data/surfaces/property-panel.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { Input } from \"../../primitives/index.js\";\nimport { accessibleName, spaceValue } from \"../../primitives/core/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Arr from \"effect/Array\";\nimport { recordsOf } from \"../internal/list.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { useTheme } from \"../theme.js\";\nimport { KeyValue, type KeyValueEntry } from \"./key-value.js\";\nimport { propertyName } from \"./logic.js\";\nimport { SURFACE_HEADER, SURFACE_PANEL } from \"./recipe.js\";\n\nexport type PropertyRow = ReadonlyPropertyRow | EditablePropertyRow;\n\nexport interface ReadonlyPropertyRow {\n  readonly kind: \"readonly\";\n\n  readonly key: string;\n  readonly label: string;\n  readonly value: string;\n}\n\nexport interface EditablePropertyRow {\n  readonly kind: \"editable\";\n\n  readonly key: string;\n  readonly label: string;\n  readonly value: string;\n\n  readonly onChange: (next: string) => void;\n}\n\nexport interface PropertyPanelProps extends Omit<Props, \"children\"> {\n  readonly title?: string;\n\n  readonly rows: ReadonlyArray<PropertyRow>;\n\n  readonly labelWidth?: number;\n  readonly className?: string;\n}\n\nexport const isEditable = (row: PropertyRow): row is EditablePropertyRow =>\n  row.kind === \"editable\";\n\nexport const PropertyPanel = ({\n  title,\n  rows,\n  labelWidth,\n  className,\n  style,\n  ...rest\n}: PropertyPanelProps) => {\n  const theme = useTheme();\n  const heading = accessibleName(title);\n  const entries: ReadonlyArray<KeyValueEntry> = Arr.map(\n    recordsOf(rows),\n    (row) => {\n      const name = propertyName(row.label, row.key);\n      return isEditable(row)\n        ? {\n            key: row.key,\n            label: name,\n            value: (\n              <Input\n                label={name}\n                value={row.value}\n                onValueChange={row.onChange}\n                size=\"sm\"\n                fullWidth\n                testId={`property-${row.key}`}\n              />\n            ),\n          }\n        : {\n            key: row.key,\n            label: name,\n            value: row.value,\n          };\n    },\n  );\n  return (\n    <div\n      {...rest}\n      role=\"group\"\n      style={mergeStyles(\n        recipeStyle(cn(SURFACE_PANEL, className), theme),\n        style,\n      )}\n    >\n      {heading === undefined ? undefined : (\n        <div style={recipeStyle(SURFACE_HEADER, theme)}>\n          <text\n            role=\"heading\"\n            aria-level={3}\n            style={recipeStyle(\"text-xs font-medium\", theme)}\n          >\n            {heading}\n          </text>\n        </div>\n      )}\n      <div\n        style={{\n          paddingLeft: spaceValue(theme, \"sm\"),\n          paddingRight: spaceValue(theme, \"sm\"),\n        }}\n      >\n        <KeyValue entries={entries} labelWidth={labelWidth} />\n      </div>\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/surfaces/recipe.ts",
      "target": "@ui/gpuixui/data/surfaces/recipe.ts",
      "type": "registry:lib",
      "content": "export const SURFACE_PANEL =\n  \"flex flex-col rounded-lg border border-input bg-card text-foreground\";\n\nexport const SURFACE_HEADER =\n  \"flex flex-row items-center justify-between gap-2 border-b border-input px-3 py-2\";\n\nexport const KEY_VALUE_ROW = \"flex flex-row items-center gap-2 py-1\";\n\nexport const KEY_VALUE_LABEL = \"text-xs text-muted-foreground\";\n\nexport const KEY_VALUE_VALUE = \"text-xs text-foreground\";\n\nexport const STAT_CARD =\n  \"flex flex-col gap-1 rounded-lg border border-input bg-card px-3 py-2\";\n\nexport const STAT_VALUE = \"text-2xl font-semibold text-foreground\";\n\nexport const STAT_LABEL = \"text-xs text-muted-foreground\";\n\nexport const STAT_HINT = \"text-xs text-muted-foreground\";\n\nexport const DATA_TOOLBAR =\n  \"flex flex-row items-center gap-2 rounded-md border border-input bg-muted px-2 py-1\";\n\nexport const FILTER_CLAUSE = \"flex flex-row items-center gap-2 py-1\";\n\nexport const FILTER_GROUP = \"flex flex-col gap-1 border-l border-input pl-2\";\n\nexport const ADVANCED_FILTER =\n  \"flex flex-col gap-2 rounded-lg border border-input bg-card px-3 py-2\";\n\nexport const FILTER_SENTENCE = \"text-xs text-foreground\";\n"
    },
    {
      "path": "packages/data/src/surfaces/stat-card.tsx",
      "target": "@ui/gpuixui/data/surfaces/stat-card.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { spaceValue, visibleCopy } from \"../../primitives/core/index.js\";\nimport { statusTone } from \"../../tokens/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport type { ReactNode } from \"react\";\nimport { readable } from \"../internal/readable.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { themeColors, useTheme } from \"../theme.js\";\nimport { STAT_CARD, STAT_HINT, STAT_LABEL, STAT_VALUE } from \"./recipe.js\";\n\nexport interface StatTrend {\n  readonly direction: \"up\" | \"down\" | \"flat\";\n\n  readonly text: string | null;\n}\n\nexport interface StatCardProps extends Omit<Props, \"children\"> {\n  readonly label: string;\n\n  readonly value: ReactNode;\n\n  readonly unit?: string;\n\n  readonly hint?: ReactNode;\n\n  readonly trend?: StatTrend;\n  readonly className?: string;\n}\n\nexport const trendTone = (\n  direction: StatTrend[\"direction\"],\n  theme: ReturnType<typeof useTheme>,\n): string | undefined =>\n  direction === \"up\"\n    ? statusTone(theme, \"success\").foreground\n    : direction === \"down\"\n      ? statusTone(theme, \"destructive\").foreground\n      : themeColors(theme).mutedForeground;\n\nexport const StatCard = ({\n  label,\n  value,\n  unit,\n  hint,\n  trend,\n  className,\n  style,\n  ...rest\n}: StatCardProps) => {\n  const theme = useTheme();\n\n  const movement = Option.fromNullishOr(trend);\n\n  const title = visibleCopy(label);\n  const unitCopy = visibleCopy(unit);\n  const hintCopy = P.isString(hint) ? visibleCopy(hint) : hint;\n  return (\n    <div\n      {...rest}\n      role=\"group\"\n      style={mergeStyles(recipeStyle(cn(STAT_CARD, className), theme), style)}\n    >\n      {title === undefined ? undefined : (\n        <text style={recipeStyle(STAT_LABEL, theme)}>{title}</text>\n      )}\n      <div\n        style={{\n          display: \"flex\",\n          flexDirection: \"row\",\n          alignItems: \"baseline\",\n          gap: spaceValue(theme, \"xs\"),\n        }}\n      >\n        {readable(value, { style: recipeStyle(STAT_VALUE, theme) })}\n        {unitCopy === undefined ? undefined : (\n          <text style={recipeStyle(STAT_HINT, theme)}>{unitCopy}</text>\n        )}\n        {Option.match(movement, {\n          onNone: () => undefined,\n          onSome: (shown) => {\n            const words = visibleCopy(shown.text ?? undefined);\n            return words === undefined ? undefined : (\n              <text\n                style={{\n                  ...recipeStyle(STAT_HINT, theme),\n                  color: trendTone(shown.direction, theme),\n                }}\n              >\n                {words}\n              </text>\n            );\n          },\n        })}\n      </div>\n      {hintCopy === undefined\n        ? undefined\n        : readable(hintCopy, { style: recipeStyle(STAT_HINT, theme) })}\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/surfaces/toolbar.tsx",
      "target": "@ui/gpuixui/data/surfaces/toolbar.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { Input } from \"../../primitives/index.js\";\nimport { spokenText, visibleCopy } from \"../../primitives/core/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport { Debouncer } from \"@tanstack/pacer\";\nimport * as Option from \"effect/Option\";\nimport { useEffect, useMemo, useRef, type ReactNode } from \"react\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { useTheme } from \"../theme.js\";\nimport { DATA_TOOLBAR } from \"./recipe.js\";\n\nexport interface ToolbarSearch {\n  readonly value: string;\n\n  readonly onValueChange: (next: string) => void;\n\n  readonly placeholder?: string;\n\n  readonly label?: string;\n\n  readonly debounceMs?: number;\n}\n\nexport interface DataToolbarProps extends Omit<Props, \"children\"> {\n  readonly children?: ReactNode;\n\n  readonly search?: ToolbarSearch;\n  readonly className?: string;\n\n  readonly testId?: string;\n}\n\nexport const DEFAULT_SEARCH_DEBOUNCE_MS = 150;\n\nexport const DEFAULT_SEARCH_LABEL = \"Search\";\n\nexport const DEFAULT_SEARCH_PLACEHOLDER = \"Search…\";\n\nconst searchWait = (milliseconds: number | undefined): number =>\n  milliseconds === undefined ||\n  !Number.isFinite(milliseconds) ||\n  milliseconds < 0\n    ? DEFAULT_SEARCH_DEBOUNCE_MS\n    : milliseconds;\n\nexport const DataToolbar = ({\n  children,\n  search,\n  className,\n  style,\n  testId,\n  ...rest\n}: DataToolbarProps) => {\n  const theme = useTheme();\n\n  const field = Option.fromNullishOr(search);\n  const wait = searchWait(search?.debounceMs);\n\n  const report = useRef<(next: string) => void>(() => {});\n  report.current = search?.onValueChange ?? (() => {});\n  const debouncer = useMemo(\n    () => new Debouncer((next: string) => report.current(next), { wait }),\n    [wait],\n  );\n\n  useEffect(() => () => debouncer.cancel(), [debouncer]);\n\n  return (\n    <div\n      {...rest}\n      style={mergeStyles(\n        recipeStyle(cn(DATA_TOOLBAR, className), theme),\n        style,\n      )}\n    >\n      {Option.match(field, {\n        onNone: () => undefined,\n        onSome: (query) => (\n          <Input\n            label={spokenText(\n              Option.fromNullishOr(query.label),\n              DEFAULT_SEARCH_LABEL,\n            )}\n            placeholder={\n              visibleCopy(query.placeholder) ?? DEFAULT_SEARCH_PLACEHOLDER\n            }\n            value={query.value}\n            onValueChange={(next) => {\n              if (wait <= 0) report.current(next);\n              else debouncer.maybeExecute(next);\n            }}\n            size=\"sm\"\n            testId={\n              testId === undefined ? \"toolbar-search\" : `${testId}-search`\n            }\n          />\n        ),\n      })}\n      {children}\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/table/expandable.tsx",
      "target": "@ui/gpuixui/data/table/expandable.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport { Icon } from \"../../icons/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { useGpuix, type Props, type PublicInstance } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\nimport { useCallback, useRef, type ReactNode } from \"react\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { themeColors, useTheme } from \"../theme.js\";\nimport { TABLE_DETAIL_CELL, TABLE_DETAIL_ROW } from \"./recipe.js\";\nimport { TableCell, TableRow, type TableRowProps } from \"./table.js\";\n\nexport const DEFAULT_EXPANDER_WIDTH = 32;\n\nexport interface TableDetailRowProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const TableDetailRow = ({\n  className,\n  children,\n  testId,\n  style,\n  ...rest\n}: TableDetailRowProps) => {\n  const theme = useTheme();\n  return (\n    <div\n      {...rest}\n      testId={testId}\n      role=\"row\"\n      style={mergeStyles(\n        recipeStyle(cn(TABLE_DETAIL_ROW, className), theme),\n        style,\n      )}\n    >\n      <div\n        role=\"cell\"\n        testId={testId === undefined ? undefined : `${testId}-cell`}\n        style={{\n          ...recipeStyle(TABLE_DETAIL_CELL, theme),\n          flexGrow: 1,\n          flexShrink: 1,\n          flexBasis: 0,\n          minWidth: 0,\n        }}\n      >\n        {children}\n      </div>\n    </div>\n  );\n};\n\nexport interface TableExpandableRowProps extends Omit<\n  TableRowProps,\n  \"children\" | \"onClick\"\n> {\n  readonly expanded?: boolean;\n\n  readonly onToggle?: () => void;\n\n  readonly cells: ReactNode;\n\n  readonly detail: ReactNode;\n\n  readonly toggleWidth?: number;\n\n  readonly detailTestId?: string;\n}\n\nexport const TableExpandableRow = ({\n  expanded,\n  onToggle,\n  cells,\n  detail,\n  toggleWidth = DEFAULT_EXPANDER_WIDTH,\n  detailTestId,\n  disabled,\n  className,\n  style,\n  tabIndex,\n  onKeyDown,\n  instanceRef,\n  ...rest\n}: TableExpandableRowProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const renderer = useGpuix().renderer;\n  const row = useRef<PublicInstance | null>(null);\n  const attach = useCallback(\n    (instance: PublicInstance | null) => {\n      row.current = instance;\n      if (typeof instanceRef === \"function\") instanceRef(instance);\n      else if (instanceRef !== null && instanceRef !== undefined)\n        instanceRef.current = instance;\n    },\n    [instanceRef],\n  );\n  const open = expanded === true;\n  return (\n    <>\n      <TableRow\n        {...rest}\n        instanceRef={attach}\n        disabled={disabled}\n        aria-expanded={open}\n        tabIndex={disabled === true ? -1 : (tabIndex ?? 0)}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          if (disabled === true) return;\n          const ours = Option.fromNullishOr(row.current?.id);\n          const focused = Option.fromNullishOr(\n            renderer?.getFocusedElementId?.(),\n          );\n          if (!Option.exists(focused, (id) => Option.contains(ours, id)))\n            return;\n          if (event.key === \"enter\" || event.key === \"space\") onToggle?.();\n        }}\n        className={className}\n        style={style}\n        onClick={onToggle}\n      >\n        <TableCell\n          width={toggleWidth}\n          align=\"center\"\n          style={{ paddingLeft: 0, paddingRight: 0 }}\n        >\n          {}\n          {colors.mutedForeground === undefined ? undefined : (\n            <Icon\n              name={open ? \"chevron-down\" : \"chevron-right\"}\n              size=\"sm\"\n              color={colors.mutedForeground}\n            />\n          )}\n        </TableCell>\n        {cells}\n      </TableRow>\n      {open ? (\n        <TableDetailRow testId={detailTestId}>{detail}</TableDetailRow>\n      ) : undefined}\n    </>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/table/frame.tsx",
      "target": "@ui/gpuixui/data/table/frame.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport type { EventPayload } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\n\nexport const DEFAULT_HEADER_HEIGHT = 44;\n\nexport interface TableFrame {\n  readonly viewportHeight: Option.Option<number>;\n\n  readonly headerHeight: Option.Option<number>;\n  readonly reportViewport: (height: number) => void;\n  readonly reportHeader: (height: number) => void;\n}\n\nconst TableFrameContext = createContext<Option.Option<TableFrame>>(\n  Option.none(),\n);\n\nexport const useTableFrame = (): Option.Option<TableFrame> =>\n  useContext(TableFrameContext);\n\nexport const useMeasuredBodyHeight = (): Option.Option<number> => {\n  const frame = useTableFrame();\n  return Option.flatMap(frame, (value) =>\n    Option.map(value.viewportHeight, (viewport) =>\n      Math.max(0, viewport - Option.getOrElse(value.headerHeight, () => 0)),\n    ),\n  );\n};\n\nconst useHeightReport = (\n  initial?: number,\n): [Option.Option<number>, (height: number) => void] => {\n  const [height, setHeight] = useState<Option.Option<number>>(\n    Option.fromNullishOr(initial),\n  );\n  const report = useCallback((next: number) => {\n    if (!Number.isFinite(next) || next <= 0) return;\n    setHeight((current) =>\n      Option.contains(current, next) ? current : Option.some(next),\n    );\n  }, []);\n  return [height, report];\n};\n\nexport const TableFrameProvider = ({\n  children,\n  initialHeight,\n}: {\n  readonly children: ReactNode;\n\n  readonly initialHeight?: number;\n}) => {\n  const [headerHeight, reportHeader] = useHeightReport(DEFAULT_HEADER_HEIGHT);\n  const [viewportHeight, reportViewport] = useHeightReport(initialHeight);\n  const frame = useMemo<TableFrame>(\n    () => ({\n      headerHeight,\n      viewportHeight,\n      reportHeader,\n      reportViewport,\n    }),\n    [headerHeight, viewportHeight, reportHeader, reportViewport],\n  );\n  return (\n    <TableFrameContext.Provider value={Option.some(frame)}>\n      {children}\n    </TableFrameContext.Provider>\n  );\n};\n\nexport const reportBoth = (\n  event: EventPayload,\n  caller: ((event: EventPayload) => void) | undefined,\n  report: (height: number) => void,\n): void => {\n  caller?.(event);\n  report(event.height ?? 0);\n};\n"
    },
    {
      "path": "packages/data/src/table/frozen.tsx",
      "target": "@ui/gpuixui/data/table/frozen.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport {\n  useGpuix,\n  type EventPayload,\n  type Props,\n  type PublicInstance,\n} from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport * as Option from \"effect/Option\";\nimport { Fragment, useRef, useState, type ReactNode } from \"react\";\nimport { rowsOf } from \"../internal/list.js\";\nimport { paneFill, usePaintedHeight } from \"../internal/painted.js\";\nimport { paintableSize } from \"../internal/size.js\";\nimport { DEFAULT_HEADER_HEIGHT } from \"./frame.js\";\nimport {\n  TableBody,\n  TableEmpty,\n  TableHeader,\n  TableViewport,\n  emptyMessage,\n} from \"./table.js\";\n\nexport interface FrozenTableProps<Row> extends Omit<Props, \"children\"> {\n  readonly height: number;\n\n  readonly frozenWidth: number;\n\n  readonly contentWidth: number;\n\n  readonly rows: ReadonlyArray<Row>;\n\n  readonly rowKey: (row: Row) => string;\n\n  readonly frozenHeader: ReactNode;\n\n  readonly frozenRow: (row: Row) => ReactNode;\n\n  readonly header: ReactNode;\n\n  readonly row: (row: Row) => ReactNode;\n\n  readonly empty?: ReactNode;\n\n  readonly frozenTestId?: string;\n\n  readonly bodyTestId?: string;\n\n  readonly viewportTestId?: string;\n}\n\nexport const FrozenTable = <Row,>({\n  height,\n  frozenWidth,\n  contentWidth,\n  rows,\n  rowKey,\n  frozenHeader,\n  frozenRow,\n  header,\n  row,\n  empty,\n  frozenTestId,\n  bodyTestId,\n  viewportTestId,\n  onLayout,\n  style,\n  ...rest\n}: FrozenTableProps<Row>) => {\n  const renderer = useGpuix().renderer;\n  const frozen = useRef<PublicInstance | null>(null);\n  const body = useRef<PublicInstance | null>(null);\n  const syncing = useRef(false);\n  const bodyX = useRef(0);\n\n  const bound = paintableSize(height);\n  const painted = usePaintedHeight(onLayout);\n  const box = Option.orElse(bound, () => painted.measured);\n  const boxHeight = Option.getOrUndefined(box);\n\n  const frozenTrack = Option.getOrUndefined(paintableSize(frozenWidth));\n  const contentTrack = Option.getOrUndefined(paintableSize(contentWidth));\n\n  const records = rowsOf(rows, rowKey);\n  const [frozenHeaderHeight, setFrozenHeaderHeight] = useState(0);\n  const [dataHeaderHeight, setDataHeaderHeight] = useState(0);\n\n  const measuredHeader = Math.max(frozenHeaderHeight, dataHeaderHeight);\n  const headerHeight =\n    measuredHeader === 0 ? DEFAULT_HEADER_HEIGHT : measuredHeader;\n  const bodyHeight = Option.getOrUndefined(\n    Option.map(box, (size) => Math.max(0, size - headerHeight)),\n  );\n\n  const mirror = (from: \"frozen\" | \"body\") => (event: EventPayload) => {\n    const y = event.scrollY;\n    if (from === \"body\" && event.scrollX !== undefined) {\n      bodyX.current = event.scrollX;\n    }\n    if (y === undefined || syncing.current) return;\n    const target = from === \"body\" ? frozen : body;\n    const id = target.current?.id;\n    if (id === undefined) return;\n    syncing.current = true;\n    renderer?.scrollTo?.(id, from === \"body\" ? 0 : bodyX.current, y);\n    syncing.current = false;\n  };\n\n  return (\n    <div\n      {...rest}\n      onLayout={painted.onLayout}\n      role=\"table\"\n      style={mergeStyles(\n        {\n          ...paneFill(bound),\n          display: \"flex\",\n          flexDirection: \"row\",\n          minHeight: 0,\n          overflow: \"hidden\",\n          ...(boxHeight === undefined ? {} : { height: boxHeight }),\n        },\n        style,\n      )}\n    >\n      <div\n        style={{\n          flexGrow: 0,\n          flexShrink: 0,\n          ...(frozenTrack === undefined\n            ? {}\n            : { width: frozenTrack, flexBasis: frozenTrack }),\n          display: \"flex\",\n          flexDirection: \"column\",\n          height: boxHeight,\n          minHeight: 0,\n        }}\n      >\n        <TableHeader\n          style={{ minHeight: headerHeight }}\n          onLayout={(event) => setFrozenHeaderHeight(event.height ?? 0)}\n        >\n          {frozenHeader}\n        </TableHeader>\n        <TableBody\n          height={bodyHeight}\n          testId={frozenTestId}\n          ref={frozen}\n          onScrollChange={mirror(\"frozen\")}\n        >\n          {records.map((record) => (\n            <Fragment key={rowKey(record)}>{frozenRow(record)}</Fragment>\n          ))}\n        </TableBody>\n      </div>\n      <div\n        style={{\n          flexGrow: 1,\n          flexShrink: 1,\n          flexBasis: 0,\n          minWidth: 0,\n          height: boxHeight,\n          display: \"flex\",\n          flexDirection: \"column\",\n        }}\n      >\n        <TableViewport\n          testId={viewportTestId}\n          style={{ flexGrow: 1, minHeight: 0 }}\n        >\n          <div\n            style={{\n              flexGrow: 0,\n              flexShrink: 0,\n              ...(contentTrack === undefined\n                ? {}\n                : { width: contentTrack, flexBasis: contentTrack }),\n              display: \"flex\",\n              flexDirection: \"column\",\n              height: boxHeight,\n              minHeight: 0,\n            }}\n          >\n            <TableHeader\n              style={{ minHeight: headerHeight }}\n              onLayout={(event) => setDataHeaderHeight(event.height ?? 0)}\n            >\n              {header}\n            </TableHeader>\n            <TableBody\n              height={bodyHeight}\n              testId={bodyTestId}\n              ref={body}\n              onScrollChange={mirror(\"body\")}\n            >\n              {records.length === 0 ? (\n                <TableEmpty>{emptyMessage(empty)}</TableEmpty>\n              ) : (\n                records.map((record) => (\n                  <Fragment key={rowKey(record)}>{row(record)}</Fragment>\n                ))\n              )}\n            </TableBody>\n          </div>\n        </TableViewport>\n      </div>\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/table/recipe.ts",
      "target": "@ui/gpuixui/data/table/recipe.ts",
      "type": "registry:lib",
      "content": "export const TABLE_ROOT =\n  \"flex flex-col overflow-hidden rounded-lg border border-input bg-card text-foreground\";\n\nexport const TABLE_VIEWPORT = \"flex flex-col flex-1 min-h-0\";\n\nexport const TABLE_HEADER = \"flex flex-col shrink-0\";\n\nexport const TABLE_BODY = \"flex flex-col flex-1 min-h-0\";\n\nexport const TABLE_ROW = \"flex flex-row items-center shrink-0\";\n\nexport const TABLE_ROW_SELECTED =\n  \"flex flex-row items-center shrink-0 bg-selected text-selected-foreground\";\n\nexport const TABLE_ROW_INTERACTIVE =\n  \"flex flex-row items-center shrink-0 hover:bg-hover cursor-pointer\";\n\nexport const TABLE_HEADER_CELL =\n  \"relative flex flex-row items-center px-3 py-2 text-sm font-medium text-muted-foreground\";\n\nexport const TABLE_COLUMN_DIVIDER =\n  \"absolute inset-y-0 right-0 w-1.5 bg-border hover:bg-primary\";\n\nexport const TABLE_CELL = \"px-3 py-2 text-sm\";\n\nexport const TABLE_DETAIL_ROW =\n  \"flex flex-row items-stretch shrink-0 bg-muted text-muted-foreground\";\n\nexport const TABLE_DETAIL_CELL = \"px-3 py-3 text-sm\";\n\nexport const TABLE_CAPTION = \"px-3 py-2 text-xs text-muted-foreground\";\n\nexport const TABLE_EMPTY =\n  \"flex flex-col items-center justify-center gap-1 px-3 py-6\";\n\nexport const TABLE_SKELETON_BAR = \"h-2 rounded-sm bg-skeleton\";\n"
    },
    {
      "path": "packages/data/src/table/table.tsx",
      "target": "@ui/gpuixui/data/table/table.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props, PublicInstance, StyleDesc } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport {\n  borderWidthOf,\n  spaceValue,\n  spokenText,\n} from \"../../primitives/core/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport type { ReactNode, Ref } from \"react\";\nimport { paintableSize } from \"../internal/size.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport {\n  dividerOn,\n  pointerWash,\n  textStyle,\n  themeColors,\n  useTheme,\n} from \"../theme.js\";\nimport {\n  reportBoth,\n  TableFrameProvider,\n  useMeasuredBodyHeight,\n  useTableFrame,\n} from \"./frame.js\";\nimport {\n  TABLE_BODY,\n  TABLE_CAPTION,\n  TABLE_CELL,\n  TABLE_EMPTY,\n  TABLE_HEADER,\n  TABLE_HEADER_CELL,\n  TABLE_ROOT,\n  TABLE_ROW,\n  TABLE_ROW_INTERACTIVE,\n  TABLE_ROW_SELECTED,\n  TABLE_SKELETON_BAR,\n  TABLE_VIEWPORT,\n} from \"./recipe.js\";\n\nexport type CellAlign = \"left\" | \"center\" | \"right\";\n\nconst JUSTIFY: Readonly<Record<CellAlign, string>> = {\n  left: \"flex-start\",\n  center: \"center\",\n  right: \"flex-end\",\n};\n\nexport interface CellLayoutProps {\n  readonly width?: number;\n\n  readonly align?: CellAlign;\n\n  readonly className?: string;\n}\n\nconst cellLayout = ({ width, align = \"left\" }: CellLayoutProps): StyleDesc => {\n  const track = Option.getOrUndefined(paintableSize(width));\n  return {\n    ...(track === undefined\n      ? { flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0 }\n      : { width: track, flexGrow: 0, flexShrink: 0, flexBasis: track }),\n    justifyContent: JUSTIFY[align],\n  };\n};\n\nexport interface TableProps extends Omit<Props, \"children\"> {\n  readonly height?: number;\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const Table = ({\n  height,\n  className,\n  children,\n  style,\n  ...rest\n}: TableProps) => {\n  const theme = useTheme();\n  const bound = Option.getOrUndefined(paintableSize(height));\n  return (\n    <TableFrameProvider initialHeight={bound}>\n      <div\n        {...rest}\n        role=\"table\"\n        style={mergeStyles(\n          mergeStyles(\n            recipeStyle(cn(TABLE_ROOT, className), theme),\n            bound === undefined ? undefined : { height: bound },\n          ),\n          style,\n        )}\n      >\n        {children}\n      </div>\n    </TableFrameProvider>\n  );\n};\n\nexport interface TableViewportProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const TableViewport = ({\n  className,\n  children,\n  style,\n  onLayout,\n  ...rest\n}: TableViewportProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const frame = useTableFrame();\n  return (\n    <div\n      {...rest}\n      onLayout={(event) =>\n        reportBoth(\n          event,\n          onLayout,\n          Option.match(frame, {\n            onNone: () => () => {},\n            onSome: (measured) => measured.reportViewport,\n          }),\n        )\n      }\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(TABLE_VIEWPORT, className), theme), {\n          overflowX: \"scroll\",\n          scrollbarColor: colors.scrollbar,\n        }),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface TableHeaderProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const TableHeader = ({\n  className,\n  children,\n  style,\n  onLayout,\n  ...rest\n}: TableHeaderProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const frame = useTableFrame();\n  return (\n    <div\n      {...rest}\n      onLayout={(event) =>\n        reportBoth(\n          event,\n          onLayout,\n          Option.match(frame, {\n            onNone: () => () => {},\n            onSome: (measured) => measured.reportHeader,\n          }),\n        )\n      }\n      role=\"rowgroup\"\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(TABLE_HEADER, className), theme), {\n          borderBottomWidth: borderWidthOf(theme, \"thin\"),\n\n          borderColor: dividerOn(theme, colors.card) ?? colors.border,\n        }),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface TableBodyProps extends Omit<Props, \"children\"> {\n  readonly rows?: number;\n\n  readonly height?: number;\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const TableBody = ({\n  rows = 3,\n  height,\n  className,\n  children,\n  style,\n  ...rest\n}: TableBodyProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const empty = children === undefined || children === null;\n  const measured = useMeasuredBodyHeight();\n  const definite = Option.orElse(paintableSize(height), () => measured);\n  return (\n    <div\n      {...rest}\n      role=\"rowgroup\"\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(TABLE_BODY, className), theme), {\n          overflowY: \"scroll\",\n          scrollbarColor: colors.scrollbar,\n          ...(Option.isNone(definite)\n            ? {}\n            : {\n                height: definite.value,\n                flexGrow: 0,\n                flexShrink: 0,\n                flexBasis: definite.value,\n              }),\n        }),\n        style,\n      )}\n    >\n      {empty ? <TableSkeleton rows={rows} /> : children}\n    </div>\n  );\n};\n\nexport interface TableRowProps extends Omit<Props, \"children\"> {\n  readonly selected?: boolean;\n\n  readonly disabled?: boolean;\n\n  readonly instanceRef?: Ref<PublicInstance>;\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const TableRow = ({\n  selected,\n  disabled,\n  instanceRef,\n  className,\n  children,\n  style,\n  onClick,\n  ...rest\n}: TableRowProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const interactive = onClick !== undefined && disabled !== true;\n  const recipe =\n    selected === true\n      ? TABLE_ROW_SELECTED\n      : interactive\n        ? TABLE_ROW_INTERACTIVE\n        : TABLE_ROW;\n\n  const rule = (surface: string | undefined): string | undefined =>\n    dividerOn(theme, surface) ?? colors.border;\n  const hoverFill = selected === true ? colors.accent : colors.hover;\n  const pressFill =\n    (selected === true\n      ? pointerWash(theme, \"active\", \"accent\")\n      : pointerWash(theme, \"active\", \"hover\")) ?? colors.active;\n  return (\n    <div\n      {...rest}\n      ref={instanceRef}\n      role=\"row\"\n      aria-selected={selected}\n      onClick={disabled === true ? undefined : onClick}\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(recipe, className), theme), {\n          borderBottomWidth: borderWidthOf(theme, \"thin\"),\n          borderColor: rule(selected === true ? colors.selected : colors.card),\n\n          ...(interactive\n            ? {\n                cursor: \"pointer\",\n\n                hover:\n                  theme.interaction === \"appkit\"\n                    ? {}\n                    : {\n                        backgroundColor: hoverFill,\n                        borderColor: rule(hoverFill),\n                      },\n                active: {\n                  backgroundColor: pressFill,\n                  borderColor: rule(pressFill),\n                },\n              }\n            : {}),\n        }),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface TableHeaderCellProps\n  extends Omit<Props, \"children\">, CellLayoutProps {\n  readonly children?: ReactNode;\n}\n\nexport const TableHeaderCell = ({\n  width,\n  align = \"left\",\n  className,\n  children,\n  style,\n  ...rest\n}: TableHeaderCellProps) => {\n  const theme = useTheme();\n  return (\n    <div\n      {...rest}\n      role=\"columnheader\"\n      style={mergeStyles(\n        mergeStyles(\n          recipeStyle(cn(TABLE_HEADER_CELL, className), theme),\n          cellLayout({ width, align }),\n        ),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface TableCellProps\n  extends Omit<Props, \"children\">, CellLayoutProps {\n  readonly children?: ReactNode;\n}\n\nexport const TableCell = ({\n  width,\n  align = \"left\",\n  className,\n  children,\n  style,\n  ...rest\n}: TableCellProps) => {\n  const theme = useTheme();\n  return (\n    <div\n      {...rest}\n      role=\"cell\"\n      style={mergeStyles(\n        mergeStyles(\n          recipeStyle(cn(TABLE_CELL, className), theme),\n          cellLayout({ width, align }),\n        ),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface TableCaptionProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const TableCaption = ({\n  className,\n  children,\n  style,\n  ...rest\n}: TableCaptionProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  return (\n    <div\n      {...rest}\n      style={mergeStyles(\n        recipeStyle(cn(TABLE_CAPTION, className), theme),\n        style,\n      )}\n    >\n      {typeof children === \"string\" || typeof children === \"number\" ? (\n        <text\n          style={{\n            ...textStyle(theme, \"caption\"),\n            color: colors.mutedForeground,\n          }}\n        >\n          {children}\n        </text>\n      ) : (\n        children\n      )}\n    </div>\n  );\n};\n\nexport interface TableEmptyProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const DEFAULT_EMPTY_MESSAGE = \"No rows\";\n\nexport const emptyMessage = (empty: ReactNode): ReactNode =>\n  P.isString(empty)\n    ? spokenText(Option.fromNullishOr(empty), DEFAULT_EMPTY_MESSAGE)\n    : (empty ?? DEFAULT_EMPTY_MESSAGE);\n\nexport const TableEmpty = ({\n  className,\n  children,\n  style,\n  ...rest\n}: TableEmptyProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  return (\n    <div\n      {...rest}\n      role=\"row\"\n      style={mergeStyles(recipeStyle(cn(TABLE_EMPTY, className), theme), style)}\n    >\n      <div\n        role=\"cell\"\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          gap: spaceValue(theme, \"xs\"),\n        }}\n      >\n        {typeof children === \"string\" || typeof children === \"number\" ? (\n          <text\n            style={{\n              ...textStyle(theme, \"body\"),\n              color: colors.mutedForeground,\n            }}\n          >\n            {children}\n          </text>\n        ) : (\n          children\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport interface TableSkeletonProps {\n  readonly rows?: number;\n\n  readonly columns?: number;\n}\n\nexport const SKELETON_TRACK_LIMIT = 20;\n\nconst trackCount = (count: number, fallback: number): number =>\n  Number.isFinite(count)\n    ? Math.min(SKELETON_TRACK_LIMIT, Math.max(1, Math.floor(count)))\n    : fallback;\n\nexport const TableSkeleton = ({\n  rows = 3,\n  columns = 3,\n}: TableSkeletonProps) => {\n  const rowCount = trackCount(rows, 3);\n  const columnCount = trackCount(columns, 3);\n  return (\n    <>\n      {Array.from({ length: rowCount }, (_, row) => (\n        <TableRow key={`skeleton-${row}`}>\n          {Array.from({ length: columnCount }, (_, column) => (\n            <TableCell key={`skeleton-${row}-${column}`}>\n              <div style={{ display: \"flex\", flexDirection: \"column\" }}>\n                <SkeletonBar />\n              </div>\n            </TableCell>\n          ))}\n        </TableRow>\n      ))}\n    </>\n  );\n};\n\nconst SkeletonBar = () => {\n  const theme = useTheme();\n  return (\n    <div\n      style={mergeStyles(recipeStyle(TABLE_SKELETON_BAR, theme), {\n        width: \"60%\",\n      })}\n    />\n  );\n};\n"
    },
    {
      "path": "packages/data/src/theme.ts",
      "target": "@ui/gpuixui/data/theme.ts",
      "type": "registry:lib",
      "content": "import type { Theme } from \"../tokens/index.js\";\nimport { useTheme as useTokenTheme } from \"../tokens/react.js\";\n\nexport {\n  controlFocusRing,\n  dividerOn,\n  pointerWash,\n  textStyle,\n  themeColors,\n} from \"../tokens/index.js\";\nexport type { Theme };\n\nexport const useTheme = (): Theme => useTokenTheme();\n"
    },
    {
      "path": "packages/data/src/tree/logic.ts",
      "target": "@ui/gpuixui/data/tree/logic.ts",
      "type": "registry:lib",
      "content": "import { typeaheadMatchIndex } from \"../../components/typeahead.js\";\nimport { spokenText } from \"../../primitives/core/index.js\";\nimport { Match, pipe } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as HashSet from \"effect/HashSet\";\nimport * as MutableHashSet from \"effect/MutableHashSet\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport * as Schema from \"effect/Schema\";\nimport { listOf } from \"../internal/list.js\";\nimport { neighbourKey } from \"../selection/logic.js\";\n\nexport const TREE_INDENT = 16;\n\nexport interface TreeSpec<T> {\n  readonly keyOf: (node: T) => string;\n\n  readonly childrenOf: (node: T) => ReadonlyArray<T> | null | undefined;\n}\n\nexport interface TreeRow<T> {\n  readonly node: T;\n  readonly key: string;\n\n  readonly depth: number;\n\n  readonly parent: Option.Option<string>;\n\n  readonly hasChildren: boolean;\n\n  readonly expanded: boolean;\n}\n\nexport const flattenTree = <T>(\n  roots: ReadonlyArray<T> | null | undefined,\n  spec: TreeSpec<T>,\n  expanded: HashSet.HashSet<string>,\n): ReadonlyArray<TreeRow<T>> => {\n  const rows: Array<TreeRow<T>> = [];\n\n  const painted = MutableHashSet.empty<string>();\n\n  const visit = (\n    node: T,\n    depth: number,\n    parent: Option.Option<string>,\n    ancestors: HashSet.HashSet<string>,\n  ): void => {\n    const key = spec.keyOf(node);\n    if (HashSet.has(ancestors, key) || MutableHashSet.has(painted, key)) return;\n    MutableHashSet.add(painted, key);\n    const children = listOf(spec.childrenOf(node));\n    const hasChildren = Arr.isReadonlyArrayNonEmpty(children);\n    const open = hasChildren && HashSet.has(expanded, key);\n    rows.push({ node, key, depth, parent, hasChildren, expanded: open });\n    if (!open) return;\n    const path = HashSet.add(ancestors, key);\n    Arr.forEach(children, (child) =>\n      visit(child, depth + 1, Option.some(key), path),\n    );\n  };\n\n  Arr.forEach(listOf(roots), (root) =>\n    visit(root, 0, Option.none(), HashSet.empty()),\n  );\n  return rows;\n};\n\nexport interface PaintedRow<T> {\n  readonly row: TreeRow<T>;\n\n  readonly leaving: boolean;\n}\n\nexport const paintedRows = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  leaving: ReadonlyArray<TreeRow<T>>,\n  before: ReadonlyArray<TreeRow<T>>,\n): ReadonlyArray<PaintedRow<T>> => {\n  const visible = HashSet.fromIterable(Arr.map(rows, (row) => row.key));\n  const anchorOf = (row: TreeRow<T>): Option.Option<string> =>\n    pipe(\n      Arr.findFirstIndex(before, (candidate) => candidate.key === row.key),\n      Option.flatMap((index) =>\n        Arr.findFirst(Arr.drop(before, index + 1), (candidate) =>\n          HashSet.has(visible, candidate.key),\n        ),\n      ),\n      Option.map((candidate) => candidate.key),\n    );\n  const anchoredBefore = (key: string): ReadonlyArray<TreeRow<T>> =>\n    Arr.filter(leaving, (row) => Option.contains(anchorOf(row), key));\n\n  return [\n    ...Arr.flatMap(rows, (row) => [\n      ...Arr.map(anchoredBefore(row.key), (gone) => ({\n        row: gone,\n        leaving: true,\n      })),\n      { row, leaving: false },\n    ]),\n    ...Arr.map(\n      Arr.filter(leaving, (row) => Option.isNone(anchorOf(row))),\n      (row) => ({ row, leaving: true }),\n    ),\n  ];\n};\n\nexport const branchKeys = <T>(\n  roots: ReadonlyArray<T> | null | undefined,\n  spec: TreeSpec<T>,\n): ReadonlyArray<string> => {\n  const keys: Array<string> = [];\n  const seen = MutableHashSet.empty<string>();\n\n  const visit = (node: T, ancestors: HashSet.HashSet<string>): void => {\n    const key = spec.keyOf(node);\n    if (HashSet.has(ancestors, key) || MutableHashSet.has(seen, key)) return;\n    MutableHashSet.add(seen, key);\n    const children = listOf(spec.childrenOf(node));\n    if (Arr.isReadonlyArrayNonEmpty(children)) keys.push(key);\n    const path = HashSet.add(ancestors, key);\n    Arr.forEach(children, (child) => visit(child, path));\n  };\n\n  Arr.forEach(listOf(roots), (root) => visit(root, HashSet.empty()));\n  return keys;\n};\n\nexport const isExpanded = (\n  expanded: HashSet.HashSet<string>,\n  key: string,\n): boolean => HashSet.has(expanded, key);\n\nexport const setExpanded = (\n  expanded: HashSet.HashSet<string>,\n  key: string,\n  open: boolean,\n): HashSet.HashSet<string> =>\n  open ? HashSet.add(expanded, key) : HashSet.remove(expanded, key);\n\nexport const toggleExpanded = (\n  expanded: HashSet.HashSet<string>,\n  key: string,\n): HashSet.HashSet<string> =>\n  setExpanded(expanded, key, !isExpanded(expanded, key));\n\nexport const collapseAll = (): HashSet.HashSet<string> => HashSet.empty();\n\nexport const expandedSet = (value: unknown): HashSet.HashSet<string> =>\n  P.isIterable(value)\n    ? HashSet.fromIterable(Arr.filter(Arr.fromIterable(value), P.isString))\n    : HashSet.empty();\n\nexport const expandAll = <T>(\n  roots: ReadonlyArray<T> | null | undefined,\n  spec: TreeSpec<T>,\n): HashSet.HashSet<string> => HashSet.fromIterable(branchKeys(roots, spec));\n\nexport const expandedPath = <T>(\n  roots: ReadonlyArray<T> | null | undefined,\n  spec: TreeSpec<T>,\n  key: string,\n): Option.Option<ReadonlyArray<string>> => {\n  const walk = (\n    nodes: ReadonlyArray<T>,\n    path: ReadonlyArray<string>,\n  ): Option.Option<ReadonlyArray<string>> =>\n    Option.match(Arr.head(nodes), {\n      onNone: () => Option.none(),\n      onSome: (node) => {\n        const nodeKey = spec.keyOf(node);\n        if (Arr.contains(path, nodeKey)) {\n          return walk(Arr.drop(nodes, 1), path);\n        }\n        const next = [...path, nodeKey];\n        if (nodeKey === key) return Option.some(next);\n        const inside = walk(listOf(spec.childrenOf(node)), next);\n        return Option.isSome(inside) ? inside : walk(Arr.drop(nodes, 1), path);\n      },\n    });\n\n  return walk(listOf(roots), []);\n};\n\nexport const expandTo = <T>(\n  expanded: HashSet.HashSet<string>,\n  roots: ReadonlyArray<T> | null | undefined,\n  spec: TreeSpec<T>,\n  key: string,\n): HashSet.HashSet<string> =>\n  Option.match(expandedPath(roots, spec, key), {\n    onNone: () => expanded,\n    onSome: (path) =>\n      Arr.reduce(path, expanded, (set, ancestor) => HashSet.add(set, ancestor)),\n  });\n\nexport const treeKeys = [\n  \"up\",\n  \"down\",\n  \"home\",\n  \"end\",\n  \"right\",\n  \"left\",\n  \"enter\",\n  \"space\",\n] as const;\n\nexport const TreeKey = Schema.Literals(treeKeys);\n\nexport type TreeKey = typeof TreeKey.Type;\n\nexport const isTreeKey = Schema.is(TreeKey);\n\nexport interface TreeCursor {\n  readonly expanded: HashSet.HashSet<string>;\n  readonly active: Option.Option<string>;\n}\n\nexport const emptyCursor: TreeCursor = {\n  expanded: HashSet.empty(),\n  active: Option.none(),\n};\n\nexport const findRow = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  key: Option.Option<string>,\n): Option.Option<TreeRow<T>> =>\n  Option.flatMap(key, (wanted) =>\n    Arr.findFirst(rows, (row) => row.key === wanted),\n  );\n\nconst indexOf = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  key: Option.Option<string>,\n): Option.Option<number> =>\n  Option.flatMap(key, (wanted) =>\n    Arr.findFirstIndex(rows, (row) => row.key === wanted),\n  );\n\nconst at = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  index: number,\n): Option.Option<TreeRow<T>> => Option.fromNullishOr(rows[index]);\n\nexport const cursorRow = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  cursor: TreeCursor,\n): Option.Option<TreeRow<T>> =>\n  Option.orElse(findRow(rows, cursor.active), () => Arr.head(rows));\n\nexport const treeCursorMove = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  cursor: TreeCursor,\n  key: TreeKey,\n): TreeCursor => {\n  const order = Arr.map(rows, (row) => row.key);\n  const active = findRow(rows, cursor.active);\n\n  const focus = (next: Option.Option<string>): TreeCursor => ({\n    ...cursor,\n    active: next,\n  });\n\n  const opened = (row: TreeRow<T>, next: boolean): TreeCursor => ({\n    expanded: setExpanded(cursor.expanded, row.key, next),\n    active: Option.some(row.key),\n  });\n  return Match.value(key).pipe(\n    Match.when(\"up\", () => focus(neighbourKey(order, cursor.active, -1))),\n    Match.when(\"down\", () => focus(neighbourKey(order, cursor.active, 1))),\n    Match.when(\"home\", () => focus(Arr.head(order))),\n    Match.when(\"end\", () =>\n      focus(Option.fromNullishOr(order[order.length - 1])),\n    ),\n    Match.when(\"right\", () =>\n      Option.match(active, {\n        onNone: () => focus(Arr.head(order)),\n        onSome: (row) => {\n          const open = isExpanded(cursor.expanded, row.key);\n          if (!row.hasChildren) return focus(Option.some(row.key));\n          if (!open) return opened(row, true);\n          return Option.match(indexOf(rows, Option.some(row.key)), {\n            onNone: () => cursor,\n            onSome: (index) =>\n              Option.match(at(rows, index + 1), {\n                onNone: () => cursor,\n                onSome: (next) =>\n                  next.depth === row.depth + 1\n                    ? focus(Option.some(next.key))\n                    : cursor,\n              }),\n          });\n        },\n      }),\n    ),\n    Match.when(\"left\", () =>\n      Option.match(active, {\n        onNone: () => cursor,\n        onSome: (row) =>\n          row.hasChildren && isExpanded(cursor.expanded, row.key)\n            ? opened(row, false)\n            : Option.match(row.parent, {\n                onNone: () => cursor,\n                onSome: (parent) => focus(Option.some(parent)),\n              }),\n      }),\n    ),\n    Match.when(\"enter\", () =>\n      Option.match(active, {\n        onNone: () => cursor,\n        onSome: (row) =>\n          row.hasChildren\n            ? opened(row, !isExpanded(cursor.expanded, row.key))\n            : focus(Option.some(row.key)),\n      }),\n    ),\n    Match.when(\"space\", () =>\n      Option.match(active, {\n        onNone: () => cursor,\n        onSome: (row) =>\n          row.hasChildren\n            ? opened(row, !isExpanded(cursor.expanded, row.key))\n            : focus(Option.some(row.key)),\n      }),\n    ),\n    Match.exhaustive,\n  );\n};\n\nexport const treeName = (label: string, key: string): string =>\n  spokenText(Option.fromNullishOr(label), key);\n\nexport const typeAheadRow = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  active: Option.Option<string>,\n  prefix: string,\n  labelOf: (row: TreeRow<T>) => string,\n): Option.Option<string> =>\n  Option.flatMap(\n    typeaheadMatchIndex(\n      Arr.map(rows, (row) => ({\n        label: treeName(labelOf(row), row.key),\n        disabled: false,\n      })),\n      indexOf(rows, active),\n      prefix,\n    ),\n    (index) => Option.map(at(rows, index), (row) => row.key),\n  );\n"
    },
    {
      "path": "packages/data/src/tree/recipe.ts",
      "target": "@ui/gpuixui/data/tree/recipe.ts",
      "type": "registry:lib",
      "content": "export const TREE_ROOT = \"flex flex-col overflow-hidden rounded-md\";\n\nexport const TREE_ROW =\n  \"flex flex-row items-center shrink-0 pr-2 text-sm text-foreground hover:bg-hover cursor-pointer\";\n\nexport const TREE_ROW_ACTIVE =\n  \"flex flex-row items-center shrink-0 pr-2 text-sm bg-selected text-selected-foreground hover:bg-accent cursor-pointer\";\n\nexport const TREE_CHEVRON =\n  \"flex flex-row items-center justify-center shrink-0\";\n"
    },
    {
      "path": "packages/data/src/tree/row.tsx",
      "target": "@ui/gpuixui/data/tree/row.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { EventPayload, PublicInstance } from \"@gpuix/react\";\nimport { Icon } from \"../../icons/index.js\";\nimport {\n  iconSideOf,\n  rowHeightOf,\n  type DensitySource,\n} from \"../../primitives/core/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport type { ReactNode } from \"react\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { pointerWash, textStyle, themeColors, useTheme } from \"../theme.js\";\nimport { TREE_INDENT, type TreeRow } from \"./logic.js\";\nimport { TREE_CHEVRON, TREE_ROW, TREE_ROW_ACTIVE } from \"./recipe.js\";\n\nexport const TREE_INSET = 8;\n\nexport const treeRowHeight = (\n  height: number | undefined,\n  theme: DensitySource,\n): number => height ?? rowHeightOf(theme);\n\nexport interface TreeRowViewProps<T> {\n  readonly row: TreeRow<T>;\n\n  readonly label: string;\n\n  readonly active: boolean;\n\n  readonly tabbable: boolean;\n\n  readonly renderRow?: (row: TreeRow<T>) => ReactNode;\n\n  readonly height?: number;\n\n  readonly testId?: string;\n  readonly onPress: () => void;\n  readonly onKeyDown: (event: EventPayload) => void;\n  readonly attach: (instance: PublicInstance | null) => void;\n}\n\nconst Label = ({ active, label }: { active: boolean; label: string }) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  return (\n    <text\n      style={{\n        ...textStyle(theme, \"body\"),\n        color: active ? colors.selectedForeground : colors.foreground,\n      }}\n    >\n      {label}\n    </text>\n  );\n};\n\nexport const TreeRowView = <T,>({\n  row,\n  label,\n  active,\n  tabbable,\n  renderRow,\n  height,\n  testId,\n  onPress,\n  onKeyDown,\n  attach,\n}: TreeRowViewProps<T>) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n\n  const chevron = active ? colors.selectedForeground : colors.mutedForeground;\n  const hoverFill = active ? colors.accent : colors.hover;\n  const pressFill =\n    (active\n      ? pointerWash(theme, \"active\", \"accent\")\n      : pointerWash(theme, \"active\", \"hover\")) ?? colors.active;\n  return (\n    <div\n      ref={attach}\n      role=\"treeitem\"\n      aria-label={label}\n      aria-level={row.depth + 1}\n      aria-selected={active}\n      {...(row.hasChildren ? { \"aria-expanded\": row.expanded } : {})}\n      tabIndex={tabbable ? 0 : -1}\n      testId={testId}\n      onClick={onPress}\n      onKeyDown={onKeyDown}\n      style={{\n        ...recipeStyle(cn(active ? TREE_ROW_ACTIVE : TREE_ROW), theme),\n        height: treeRowHeight(height, theme),\n        paddingLeft: TREE_INSET + row.depth * TREE_INDENT,\n\n        hover: { backgroundColor: hoverFill },\n        active: { backgroundColor: pressFill },\n      }}\n    >\n      <div\n        testId={testId === undefined ? undefined : `${testId}-slot`}\n        style={{\n          ...recipeStyle(TREE_CHEVRON, theme),\n          width: iconSideOf(theme, \"md\"),\n          height: iconSideOf(theme, \"md\"),\n        }}\n      >\n        {row.hasChildren && chevron !== undefined ? (\n          <Icon\n            name={row.expanded ? \"chevron-down\" : \"chevron-right\"}\n            size=\"md\"\n            color={chevron}\n          />\n        ) : undefined}\n      </div>\n      {renderRow === undefined ? (\n        <Label active={active} label={label} />\n      ) : (\n        renderRow(row)\n      )}\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/tree/tree-view.tsx",
      "target": "@ui/gpuixui/data/tree/tree-view.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Option from \"effect/Option\";\nimport type { ReactNode } from \"react\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { themeColors, useTheme } from \"../theme.js\";\nimport { rowsOf } from \"../internal/list.js\";\nimport { paintableSize } from \"../internal/size.js\";\nimport { treeName, type TreeCursor, type TreeRow } from \"./logic.js\";\nimport { TREE_INSET, TreeRowView } from \"./row.js\";\nimport { TREE_ROOT } from \"./recipe.js\";\nimport { useTree } from \"./use-tree.js\";\n\nexport { TREE_INSET };\n\nexport interface TreeViewProps<T> extends Omit<Props, \"children\"> {\n  readonly rows: ReadonlyArray<TreeRow<T>>;\n\n  readonly labelOf: (node: T) => string;\n\n  readonly cursor?: TreeCursor;\n\n  readonly onCursorChange?: (cursor: TreeCursor) => void;\n\n  readonly defaultCursor?: TreeCursor;\n\n  readonly onActivate?: (row: TreeRow<T>) => void;\n\n  readonly renderRow?: (row: TreeRow<T>) => ReactNode;\n\n  readonly height?: number;\n  readonly className?: string;\n\n  readonly testId?: string;\n}\n\nexport const TreeView = <T,>({\n  rows,\n  labelOf,\n  cursor,\n  onCursorChange,\n  defaultCursor,\n  onActivate,\n  renderRow,\n  height,\n  className,\n  testId,\n  style,\n  ...rest\n}: TreeViewProps<T>) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const bound = Option.getOrUndefined(paintableSize(height));\n\n  const visibleRows = rowsOf(rows, (row) => row.key);\n  const tree = useTree({\n    rows: visibleRows,\n    labelOf,\n    cursor,\n    onCursorChange,\n    defaultCursor,\n    onActivate,\n  });\n\n  return (\n    <div\n      {...rest}\n      role=\"tree\"\n      testId={testId}\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(TREE_ROOT, className), theme), {\n          ...(bound === undefined\n            ? {}\n            : {\n                height: bound,\n                flexGrow: 0,\n                flexShrink: 0,\n                flexBasis: bound,\n              }),\n          overflowY: \"scroll\",\n          scrollbarColor: colors.scrollbar,\n        }),\n        style,\n      )}\n    >\n      {visibleRows.map((row) => (\n        <TreeRowView\n          key={row.key}\n          row={row}\n          active={Option.contains(tree.cursor.active, row.key)}\n          tabbable={Option.contains(tree.tabKey, row.key)}\n          label={treeName(labelOf(row.node), row.key)}\n          renderRow={renderRow}\n          testId={testId === undefined ? undefined : `${testId}-row-${row.key}`}\n          onPress={() => tree.activate(row)}\n          onKeyDown={tree.handleKey}\n          attach={tree.attach(row.key)}\n        />\n      ))}\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/tree/use-expanded.ts",
      "target": "@ui/gpuixui/data/tree/use-expanded.ts",
      "type": "registry:lib",
      "content": "import * as HashSet from \"effect/HashSet\";\nimport * as Option from \"effect/Option\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport {\n  collapseAll,\n  expandAll,\n  expandTo,\n  expandedSet,\n  flattenTree,\n  setExpanded,\n  toggleExpanded,\n  treeCursorMove,\n  typeAheadRow,\n  type TreeCursor,\n  type TreeKey,\n  type TreeRow,\n  type TreeSpec,\n} from \"./logic.js\";\n\nexport interface UseExpandedRowsOptions<T> extends TreeSpec<T> {\n  readonly roots: ReadonlyArray<T> | null | undefined;\n\n  readonly labelOf: (node: T) => string;\n\n  readonly initialExpanded?: Iterable<string>;\n\n  readonly initialActive?: string;\n}\n\nexport interface ExpandedRowsController<T> {\n  readonly rows: ReadonlyArray<TreeRow<T>>;\n\n  readonly cursor: TreeCursor;\n\n  readonly expanded: HashSet.HashSet<string>;\n\n  readonly active: Option.Option<string>;\n\n  readonly setCursor: (cursor: TreeCursor) => void;\n\n  readonly setActive: (key: Option.Option<string>) => void;\n\n  readonly toggle: (key: string) => void;\n\n  readonly setExpanded: (key: string, open: boolean) => void;\n\n  readonly expandAll: () => void;\n\n  readonly collapseAll: () => void;\n\n  readonly reveal: (key: string) => void;\n\n  readonly pressKey: (key: TreeKey) => void;\n\n  readonly typeAhead: (prefix: string) => void;\n}\n\nexport const useExpandedRows = <T>({\n  roots,\n  keyOf,\n  childrenOf,\n  labelOf,\n  initialExpanded,\n  initialActive,\n}: UseExpandedRowsOptions<T>): ExpandedRowsController<T> => {\n  const [cursor, setCursor] = useState<TreeCursor>(() => ({\n    expanded: expandedSet(initialExpanded),\n    active: Option.fromNullishOr(initialActive),\n  }));\n\n  const rows = useMemo(\n    () => flattenTree(roots, { keyOf, childrenOf }, cursor.expanded),\n    [roots, keyOf, childrenOf, cursor.expanded],\n  );\n\n  const setActive = useCallback(\n    (key: Option.Option<string>) =>\n      setCursor((current) => ({ ...current, active: key })),\n    [],\n  );\n\n  const toggle = useCallback(\n    (key: string) =>\n      setCursor((current) => ({\n        expanded: toggleExpanded(current.expanded, key),\n        active: Option.some(key),\n      })),\n    [],\n  );\n\n  const open = useCallback(\n    (key: string, next: boolean) =>\n      setCursor((current) => ({\n        expanded: setExpanded(current.expanded, key, next),\n        active: Option.some(key),\n      })),\n    [],\n  );\n\n  const openAll = useCallback(\n    () =>\n      setCursor((current) => ({\n        ...current,\n        expanded: expandAll(roots, { keyOf, childrenOf }),\n      })),\n    [roots, keyOf, childrenOf],\n  );\n\n  const closeAll = useCallback(\n    () => setCursor((current) => ({ ...current, expanded: collapseAll() })),\n    [],\n  );\n\n  const reveal = useCallback(\n    (key: string) =>\n      setCursor((current) => ({\n        expanded: expandTo(current.expanded, roots, { keyOf, childrenOf }, key),\n        active: Option.some(key),\n      })),\n    [roots, keyOf, childrenOf],\n  );\n\n  const pressKey = useCallback(\n    (key: TreeKey) =>\n      setCursor((current) => treeCursorMove(rows, current, key)),\n    [rows],\n  );\n\n  const typeAhead = useCallback(\n    (prefix: string) =>\n      setCursor((current) =>\n        Option.match(\n          typeAheadRow(rows, current.active, prefix, (row) =>\n            labelOf(row.node),\n          ),\n          {\n            onNone: () => current,\n            onSome: (key) => ({ ...current, active: Option.some(key) }),\n          },\n        ),\n      ),\n    [rows, labelOf],\n  );\n\n  return {\n    rows,\n    cursor,\n    expanded: cursor.expanded,\n    active: cursor.active,\n    setCursor,\n    setActive,\n    toggle,\n    setExpanded: open,\n    expandAll: openAll,\n    collapseAll: closeAll,\n    reveal,\n    pressKey,\n    typeAhead,\n  };\n};\n"
    },
    {
      "path": "packages/data/src/tree/use-tree.ts",
      "target": "@ui/gpuixui/data/tree/use-tree.ts",
      "type": "registry:lib",
      "content": "import { useGpuix, type EventPayload, type PublicInstance } from \"@gpuix/react\";\nimport { typedChar, useTypeahead } from \"../../components/typeahead.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { recordsOf } from \"../internal/list.js\";\nimport {\n  cursorRow,\n  emptyCursor,\n  isTreeKey,\n  treeCursorMove,\n  treeName,\n  type TreeCursor,\n  type TreeRow,\n} from \"./logic.js\";\n\nexport interface UseTreeOptions<T> {\n  readonly rows: ReadonlyArray<TreeRow<T>>;\n\n  readonly labelOf: (node: T) => string;\n\n  readonly cursor?: TreeCursor;\n\n  readonly onCursorChange?: (cursor: TreeCursor) => void;\n\n  readonly defaultCursor?: TreeCursor;\n\n  readonly onActivate?: (row: TreeRow<T>) => void;\n}\n\nexport interface TreeCommitOptions {\n  readonly focus?: boolean;\n}\n\nexport interface TreeController<T> {\n  readonly cursor: TreeCursor;\n\n  readonly tabKey: Option.Option<string>;\n\n  readonly commit: (cursor: TreeCursor, options?: TreeCommitOptions) => void;\n\n  readonly activate: (row: TreeRow<T>) => void;\n\n  readonly handleKey: (event: EventPayload) => void;\n\n  readonly attach: (key: string) => (instance: PublicInstance | null) => void;\n\n  readonly holdsFocus: () => boolean;\n}\n\nexport const useTree = <T>({\n  rows,\n  labelOf,\n  cursor,\n  onCursorChange,\n  defaultCursor,\n  onActivate,\n}: UseTreeOptions<T>): TreeController<T> => {\n  const renderer = useGpuix().renderer;\n\n  const visible = useMemo(() => recordsOf(rows), [rows]);\n  const [uncontrolled, setUncontrolled] = useState<TreeCursor>(\n    defaultCursor ?? emptyCursor,\n  );\n  const resolved = cursor ?? uncontrolled;\n  const elements = useRef(new Map<string, number>());\n  const focused = useRef<Option.Option<string>>(resolved.active);\n\n  const fromKeys = useRef(false);\n  const deferred = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n  useEffect(\n    () => () => {\n      if (deferred.current !== undefined) clearTimeout(deferred.current);\n    },\n    [],\n  );\n\n  const commit = useCallback(\n    (next: TreeCursor, options?: TreeCommitOptions) => {\n      fromKeys.current = options?.focus !== false;\n      if (options?.focus === false) focused.current = next.active;\n      if (cursor === undefined) setUncontrolled(next);\n      onCursorChange?.(next);\n    },\n    [cursor, onCursorChange],\n  );\n\n  const typeahead = useTypeahead(\n    Arr.map(visible, (row) => ({\n      key: row.key,\n\n      label: treeName(labelOf(row.node), row.key),\n      disabled: false,\n    })),\n    Option.flatMap(resolved.active, (key) =>\n      Arr.findFirstIndex(visible, (row) => row.key === key),\n    ),\n    (match) => commit({ ...resolved, active: Option.some(match.key) }),\n  );\n\n  useLayoutEffect(() => {\n    if (\n      Option.getOrUndefined(resolved.active) ===\n      Option.getOrUndefined(focused.current)\n    ) {\n      fromKeys.current = false;\n      return;\n    }\n    const ours = fromKeys.current;\n    fromKeys.current = false;\n    focused.current = resolved.active;\n    Option.match(resolved.active, {\n      onNone: () => {},\n      onSome: (key) => {\n        const focusRow = () => {\n          const id = elements.current.get(key);\n          if (id !== undefined) renderer?.focusElement?.(id);\n        };\n        focusRow();\n        if (ours) return;\n\n        if (deferred.current !== undefined) clearTimeout(deferred.current);\n        deferred.current = setTimeout(() => {\n          deferred.current = undefined;\n          focusRow();\n        }, 0);\n      },\n    });\n  });\n\n  const activate = useCallback(\n    (row: TreeRow<T>) => {\n      const onRow = { ...resolved, active: Option.some(row.key) };\n\n      commit(row.hasChildren ? treeCursorMove(visible, onRow, \"enter\") : onRow);\n      onActivate?.(row);\n    },\n    [commit, onActivate, resolved, visible],\n  );\n\n  const holdsFocus = useCallback((): boolean => {\n    const id = renderer?.getFocusedElementId?.();\n    if (id === undefined || id === null) return false;\n    return Arr.some(\n      Arr.fromIterable(elements.current.values()),\n      (value) => value === id,\n    );\n  }, [renderer]);\n\n  const handleKey = useCallback(\n    (event: EventPayload) => {\n      if (!holdsFocus()) return;\n      if (isTreeKey(event.key)) {\n        const next = treeCursorMove(visible, resolved, event.key);\n        commit(next);\n        if (event.key === \"enter\" || event.key === \"space\") {\n          if (onActivate !== undefined) {\n            Option.match(next.active, {\n              onNone: () => {},\n              onSome: (key) => {\n                Option.match(\n                  Arr.findFirst(visible, (candidate) => candidate.key === key),\n                  { onNone: () => {}, onSome: onActivate },\n                );\n              },\n            });\n          }\n        }\n        return;\n      }\n      Option.match(typedChar(event), {\n        onNone: () => {},\n        onSome: typeahead,\n      });\n    },\n    [commit, onActivate, resolved, visible, typeahead, holdsFocus],\n  );\n\n  const attach = useCallback(\n    (key: string) => (instance: PublicInstance | null) => {\n      if (instance === null) elements.current.delete(key);\n      else elements.current.set(key, instance.id);\n    },\n    [],\n  );\n\n  const tabKey = Option.match(cursorRow(visible, resolved), {\n    onNone: () => Option.none<string>(),\n    onSome: (row) => Option.some(row.key),\n  });\n\n  return {\n    cursor: resolved,\n    tabKey,\n    commit,\n    activate,\n    handleKey,\n    attach,\n    holdsFocus,\n  };\n};\n"
    },
    {
      "path": "packages/data/src/use-table.ts",
      "target": "@ui/gpuixui/data/use-table.ts",
      "type": "registry:lib",
      "content": "import type { EventModifiers } from \"@gpuix/react\";\nimport * as Arr from \"effect/Array\";\nimport * as HashSet from \"effect/HashSet\";\nimport * as Option from \"effect/Option\";\nimport * as R from \"effect/Record\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport type {\n  PaginationState,\n  RowData,\n  SortingState,\n} from \"@tanstack/table-core\";\nimport {\n  constructGrid,\n  gridConfig,\n  readGridState,\n  retainSorting,\n  type GridColumn,\n  type GridConfig,\n  type GridTable,\n} from \"./grid-core.js\";\nimport { filterByTree, type FilterTree } from \"./surfaces/logic.js\";\nimport { listOf } from \"./internal/list.js\";\nimport {\n  clampPageIndex,\n  pageCountOf,\n  pageSizeOf,\n  withPageSize,\n} from \"./pagination/logic.js\";\nimport {\n  allSelected,\n  clearSelection,\n  currentKey,\n  isSelected,\n  moveSelection,\n  pressMode,\n  pressSelection,\n  retainSelection,\n  selectAll,\n  selectionSize,\n  someSelected,\n  type SelectionState,\n} from \"./selection/logic.js\";\nimport {\n  pressSortOrder,\n  type SortDirection,\n  type SortOrder,\n  type SortState,\n} from \"./sort/logic.js\";\n\nexport interface UseTableOptions<Row extends RowData> {\n  readonly rows: ReadonlyArray<Row>;\n\n  readonly keyOf: (row: Row) => string;\n\n  readonly columns: ReadonlyArray<GridColumn<Row>>;\n\n  readonly hiddenColumns?: ReadonlyArray<string>;\n\n  readonly filterTree?: FilterTree;\n\n  readonly stored?: unknown;\n\n  readonly initialSort?: SortState;\n\n  readonly pageSize?: number;\n}\n\nexport interface TableController<Row extends RowData> {\n  readonly rows: ReadonlyArray<Row>;\n\n  readonly keys: ReadonlyArray<string>;\n\n  readonly sort: Option.Option<SortState>;\n\n  readonly sortOrder: SortOrder;\n  readonly query: string;\n  readonly selection: SelectionState;\n\n  readonly kept: ReadonlyArray<Row>;\n\n  readonly selectedCount: number;\n\n  readonly allSelected: boolean;\n\n  readonly someSelected: boolean;\n\n  readonly pageIndex: number;\n\n  readonly pageCount: number;\n\n  readonly pageSize: number;\n\n  readonly setPageIndex: (pageIndex: number) => void;\n\n  readonly setPageSize: (pageSize: number) => void;\n\n  readonly nextPage: () => void;\n\n  readonly previousPage: () => void;\n\n  readonly activeKey: Option.Option<string>;\n\n  readonly tabStop: Option.Option<string>;\n\n  readonly table: GridTable<Row>;\n\n  readonly save: () => GridConfig;\n  readonly setSort: (next: Option.Option<SortState>) => void;\n\n  readonly toggleSort: (field: string, additive?: boolean) => void;\n\n  readonly pressSort: (\n    field: string,\n    initialDirection: SortDirection,\n    modifiers?: EventModifiers,\n  ) => void;\n  readonly setQuery: (query: string) => void;\n  readonly press: (key: string, modifiers?: EventModifiers) => void;\n  readonly pressRow: (row: Row, modifiers?: EventModifiers) => void;\n\n  readonly toggleSelection: (key: string) => void;\n\n  readonly setAllSelected: (selected: boolean) => void;\n  readonly selectAllRows: () => void;\n  readonly clear: () => void;\n  readonly move: (delta: number) => void;\n  readonly isSelected: (key: string) => boolean;\n}\n\nconst sortingOfOrder = (order: SortOrder): SortingState =>\n  Arr.map(order, (state) => ({\n    id: state.field,\n    desc: state.direction === \"desc\",\n  }));\n\nconst sortingOf = (sort: Option.Option<SortState>): SortingState =>\n  sortingOfOrder(Option.toArray(sort));\n\nconst openingSorting = (\n  saved: SortingState,\n  initialSort: Option.Option<SortState>,\n): SortingState => (saved.length > 0 ? saved : sortingOf(initialSort));\n\nconst sortOf = (sorting: SortingState): Option.Option<SortState> =>\n  Option.map(Option.fromNullishOr(sorting[0]), (column) => ({\n    field: column.id,\n    direction: column.desc ? \"desc\" : \"asc\",\n  }));\n\nconst sortOrderOf = (sorting: SortingState): SortOrder =>\n  Arr.map(sorting, (column) => ({\n    field: column.id,\n    direction: column.desc ? \"desc\" : \"asc\",\n  }));\n\nexport const fieldValueOf =\n  <Row>(columns: ReadonlyArray<GridColumn<Row>>) =>\n  (row: Row, field: string): string | number =>\n    Option.match(\n      Arr.findFirst(columns, (column) => column.id === field),\n      {\n        onNone: () => \"\",\n        onSome: (column) => column.valueOf(row),\n      },\n    );\n\nconst rowSelectionOf = (\n  state: SelectionState,\n): Readonly<Record<string, true>> =>\n  R.fromEntries(\n    Arr.map(Arr.fromIterable(state.selected), (key) => [key, true] as const),\n  );\n\nexport const useTable = <Row extends RowData>({\n  rows,\n  keyOf,\n  columns,\n  hiddenColumns,\n  filterTree,\n  stored,\n  initialSort,\n  pageSize,\n}: UseTableOptions<Row>): TableController<Row> => {\n  const start = useMemo(() => readGridState(stored), [stored]);\n  const [sorting, setSorting] = useState<SortingState>(() =>\n    openingSorting(start.sorting, Option.fromNullishOr(initialSort)),\n  );\n  const [query, setQuery] = useState(start.globalFilter);\n  const [rowSelection, setRowSelection] = useState<\n    Readonly<Record<string, true>>\n  >(start.rowSelection);\n\n  const [pagination, setPagination] = useState<PaginationState>(() => ({\n    pageIndex: start.pagination.pageIndex,\n    pageSize:\n      pageSize === undefined ? start.pagination.pageSize : pageSizeOf(pageSize),\n  }));\n\n  const [pageTree, setPageTree] = useState<unknown>(filterTree);\n  const askedPageIndex = pageTree === filterTree ? pagination.pageIndex : 0;\n\n  const [anchor, setAnchor] = useState<Option.Option<string>>(Option.none());\n\n  const records = useMemo(() => listOf(rows), [rows]);\n\n  const source = useMemo(\n    () =>\n      Option.match(Option.fromNullishOr(filterTree), {\n        onNone: () => records,\n        onSome: (tree) => filterByTree(records, tree, fieldValueOf(columns)),\n      }),\n    [records, filterTree, columns],\n  );\n\n  const updatePage = useCallback(\n    (updater: (current: PaginationState) => PaginationState) => {\n      setPageTree(filterTree);\n      setPagination(updater);\n    },\n    [filterTree],\n  );\n  const writePage = useCallback(\n    (next: PaginationState) => {\n      setPageTree(filterTree);\n      setPagination(next);\n    },\n    [filterTree],\n  );\n  const firstPage = useCallback(\n    () =>\n      updatePage((current) =>\n        current.pageIndex === 0 ? current : { ...current, pageIndex: 0 },\n      ),\n    [updatePage],\n  );\n  const writeQuery = useCallback(\n    (next: string) => {\n      setQuery(next);\n      firstPage();\n    },\n    [firstPage],\n  );\n  const writeSorting = useCallback(\n    (next: SortingState) => {\n      setSorting(next);\n      firstPage();\n    },\n    [firstPage],\n  );\n\n  const built = useMemo(() => {\n    const build = (pageIndex: number) =>\n      constructGrid({\n        rows: source,\n        keyOf,\n        columns,\n        hiddenColumns,\n        state: {\n          sorting,\n          globalFilter: query,\n          rowSelection,\n          pagination: { pageIndex, pageSize: pagination.pageSize },\n        },\n        onSortingChange: writeSorting,\n        onGlobalFilterChange: writeQuery,\n        onRowSelectionChange: setRowSelection,\n        onPaginationChange: writePage,\n      });\n    const first = build(askedPageIndex);\n    const rowCount = first.getPrePaginatedRowModel().rows.length;\n    const pageIndex = clampPageIndex(\n      askedPageIndex,\n      rowCount,\n      pagination.pageSize,\n    );\n    return {\n      table: pageIndex === askedPageIndex ? first : build(pageIndex),\n      pageIndex,\n      pageCount: pageCountOf(rowCount, pagination.pageSize),\n      rowCount,\n    };\n  }, [\n    source,\n    keyOf,\n    columns,\n    hiddenColumns,\n    sorting,\n    query,\n    rowSelection,\n    askedPageIndex,\n    pagination.pageSize,\n    writeSorting,\n    writeQuery,\n    writePage,\n  ]);\n  const table = built.table;\n\n  const painted = table.getRowModel().rows;\n\n  const order = useMemo(\n    () => retainSorting(sorting, columns),\n    [sorting, columns],\n  );\n  const visible = useMemo(\n    () => Arr.map(painted, (row) => row.original),\n    [painted],\n  );\n  const keys = useMemo(() => Arr.map(painted, (row) => row.id), [painted]);\n  const kept = useMemo(\n    () => Arr.map(table.getFilteredRowModel().rows, (row) => row.original),\n    [table],\n  );\n\n  const keptKeys = useMemo(\n    () => Arr.map(table.getPrePaginatedRowModel().rows, (row) => row.id),\n    [table],\n  );\n\n  const chosen = useMemo<SelectionState>(\n    () => ({\n      selected: HashSet.fromIterable(table.getSelectedRowIds()),\n      anchor,\n    }),\n    [table, anchor],\n  );\n\n  const selection = useMemo(\n    () => retainSelection(chosen, keptKeys),\n    [chosen, keptKeys],\n  );\n\n  const write = useCallback((next: SelectionState) => {\n    setRowSelection(rowSelectionOf(next));\n    setAnchor(next.anchor);\n  }, []);\n\n  const sortOrder = sortOrderOf(order);\n  const writeSort = useCallback(\n    (next: SortOrder) => writeSorting(sortingOfOrder(next)),\n    [writeSorting],\n  );\n  const stepPage = useCallback(\n    (delta: number) =>\n      updatePage((current) => ({\n        ...current,\n        pageIndex: clampPageIndex(\n          built.pageIndex + delta,\n          built.rowCount,\n          current.pageSize,\n        ),\n      })),\n    [built.pageIndex, built.rowCount, updatePage],\n  );\n\n  const activeKey = useMemo(\n    () =>\n      Option.filter(currentKey(selection), (key) => Arr.contains(keys, key)),\n    [selection, keys],\n  );\n\n  return {\n    rows: visible,\n    keys,\n    kept,\n    table,\n    sort: sortOf(order),\n    sortOrder,\n    query,\n    selection,\n    selectedCount: selectionSize(selection),\n    allSelected: allSelected(selection, keys),\n    someSelected: someSelected(selection, keys),\n    pageIndex: built.pageIndex,\n    pageCount: built.pageCount,\n    pageSize: pagination.pageSize,\n    setPageIndex: (pageIndex) =>\n      updatePage((current) => ({\n        ...current,\n        pageIndex: clampPageIndex(pageIndex, built.rowCount, current.pageSize),\n      })),\n    setPageSize: (next) =>\n      updatePage((current) =>\n        withPageSize(\n          { pageIndex: built.pageIndex, pageSize: current.pageSize },\n          next,\n        ),\n      ),\n    nextPage: () => stepPage(1),\n    previousPage: () => stepPage(-1),\n    activeKey,\n    tabStop: Option.orElse(activeKey, () => Arr.head(keys)),\n    save: () =>\n      gridConfig({\n        sorting: order,\n        globalFilter: query,\n        rowSelection,\n        pagination: {\n          pageIndex: built.pageIndex,\n          pageSize: pagination.pageSize,\n        },\n      }),\n    setSort: (next) => writeSorting(sortingOf(next)),\n    toggleSort: (field, additive = false) =>\n      writeSort(pressSortOrder(sortOrder, field, \"asc\", additive)),\n    pressSort: (field, initialDirection, modifiers) =>\n      writeSort(\n        pressSortOrder(\n          sortOrder,\n          field,\n          initialDirection,\n          modifiers?.shift === true,\n        ),\n      ),\n    setQuery: writeQuery,\n    press: (key, modifiers) =>\n      write(pressSelection(chosen, key, pressMode(modifiers), keys)),\n    pressRow: (row, modifiers) =>\n      write(pressSelection(chosen, keyOf(row), pressMode(modifiers), keys)),\n    toggleSelection: (key) =>\n      write(pressSelection(chosen, key, \"toggle\", keys)),\n    setAllSelected: (selected) =>\n      write(selected ? selectAll(chosen, keys) : clearSelection(chosen)),\n    selectAllRows: () => write(selectAll(chosen, keys)),\n    clear: () => write(clearSelection(chosen)),\n    move: (delta) => write(moveSelection(chosen, keys, delta)),\n    isSelected: (key) => isSelected(selection, key),\n  };\n};\n"
    },
    {
      "path": "packages/data/src/virtual/list.tsx",
      "target": "@ui/gpuixui/data/virtual/list.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { EventPayload, Props, StyleDesc } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Option from \"effect/Option\";\nimport type { ReactNode } from \"react\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { useTheme } from \"../theme.js\";\nimport { paintableSize } from \"../internal/size.js\";\nimport { rowCount, rowHeightOf, rowIndex, overdrawRows } from \"./window.js\";\n\nexport interface VisibleRange {\n  readonly first: number;\n\n  readonly last: number;\n}\n\nexport const visibleRange = (\n  event: EventPayload,\n): Option.Option<VisibleRange> =>\n  Option.flatMap(Option.fromNullishOr(event.startIndex), (first) =>\n    Option.map(\n      Option.fromNullishOr(event.endIndex),\n      (last): VisibleRange => ({ first, last }),\n    ),\n  );\n\ninterface VirtualListShared extends Omit<Props, \"children\" | \"onVisibleRange\"> {\n  readonly height?: number;\n\n  readonly followTail?: boolean;\n\n  readonly alignment?: \"top\" | \"bottom\";\n\n  readonly overdraw?: number;\n\n  readonly onVisibleRange?: (range: VisibleRange) => void;\n\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport type VirtualListProps =\n  | (VirtualListShared & {\n      readonly itemCount?: undefined;\n      readonly estimatedItemHeight?: number;\n      readonly windowStart?: undefined;\n    })\n  | (VirtualListShared & {\n      readonly itemCount: number;\n\n      readonly estimatedItemHeight: number;\n      readonly windowStart?: number;\n    });\n\nconst listBox = (height?: number): StyleDesc =>\n  height === undefined\n    ? {}\n    : { height, flexGrow: 0, flexShrink: 0, flexBasis: height };\n\nexport const VirtualList = (props: VirtualListProps) => {\n  const theme = useTheme();\n  const {\n    height,\n    followTail,\n    alignment,\n    overdraw,\n    onVisibleRange,\n    className,\n    children,\n    style,\n    ...rest\n  } = props;\n  const handleRange =\n    onVisibleRange === undefined\n      ? undefined\n      : (event: EventPayload) =>\n          Option.match(visibleRange(event), {\n            onNone: () => {},\n            onSome: onVisibleRange,\n          });\n  const shared = {\n    ...rest,\n    followTail,\n    alignment,\n    overdraw: overdraw === undefined ? undefined : overdrawRows(overdraw),\n    onVisibleRange: handleRange,\n    style: mergeStyles(\n      recipeStyle(cn(className ?? \"\"), theme),\n      mergeStyles(listBox(Option.getOrUndefined(paintableSize(height))), style),\n    ),\n  };\n  const windowed =\n    props.itemCount === undefined\n      ? undefined\n      : {\n          itemCount: rowCount(props.itemCount),\n\n          estimatedItemHeight: rowHeightOf(props.estimatedItemHeight),\n          windowStart:\n            props.windowStart === undefined\n              ? undefined\n              : rowIndex(props.windowStart),\n        };\n  return windowed === undefined ? (\n    <virtual-list {...shared}>{children}</virtual-list>\n  ) : (\n    <virtual-list\n      {...shared}\n      itemCount={windowed.itemCount}\n      estimatedItemHeight={windowed.estimatedItemHeight}\n      windowStart={windowed.windowStart}\n    >\n      {children}\n    </virtual-list>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/virtual/table.tsx",
      "target": "@ui/gpuixui/data/virtual/table.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport { useGpuix, type Props, type PublicInstance } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport {\n  Fragment,\n  useCallback,\n  useLayoutEffect,\n  useRef,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport {\n  Table,\n  TableBody,\n  TableCaption,\n  TableEmpty,\n  TableHeader,\n  TableViewport,\n  emptyMessage,\n} from \"../table/table.js\";\nimport { useMeasuredBodyHeight } from \"../table/frame.js\";\nimport { paintableSize } from \"../internal/size.js\";\nimport { paneFill } from \"../internal/painted.js\";\nimport { VirtualList, type VisibleRange } from \"./list.js\";\nimport {\n  useVirtualRows,\n  type VirtualReveal,\n  type VirtualRowsController,\n} from \"./use-virtual-rows.js\";\nimport { anchorOffset, rowCount, rowHeightOf } from \"./window.js\";\n\nexport interface VirtualTableBodyProps extends Omit<\n  Props,\n  \"children\" | \"onVisibleRange\"\n> {\n  readonly count: number;\n\n  readonly rowHeight: number;\n\n  readonly renderRow: (index: number) => ReactNode;\n\n  readonly height?: number;\n\n  readonly overdraw?: number;\n\n  readonly reveal?: VirtualReveal;\n\n  readonly onVisibleRange?: (range: VisibleRange) => void;\n\n  readonly instanceRef?: Ref<PublicInstance>;\n  readonly testId?: string;\n}\n\nexport const VirtualTableBody = ({\n  count,\n  rowHeight: namedRowHeight,\n  renderRow,\n  height,\n  overdraw,\n  reveal,\n  onVisibleRange,\n  instanceRef,\n  testId,\n  style,\n  ...rest\n}: VirtualTableBodyProps) => {\n  const renderer = useGpuix().renderer;\n  const measured = useMeasuredBodyHeight();\n\n  const definite = Option.orElse(paintableSize(height), () => measured);\n\n  const total = rowCount(count);\n  const rowHeight = rowHeightOf(namedRowHeight);\n  const rows = useVirtualRows({\n    count: total,\n    rowHeight,\n    viewportHeight: Option.getOrElse(definite, () => 0),\n    overdraw,\n    reveal,\n  });\n  const instance = useRef<PublicInstance | null>(null);\n  const attach = useCallback(\n    (next: PublicInstance | null) => {\n      instance.current = next;\n      if (typeof instanceRef === \"function\") instanceRef(next);\n      else if (instanceRef !== undefined && instanceRef !== null)\n        instanceRef.current = next;\n    },\n    [instanceRef],\n  );\n\n  const handleRange = useCallback(\n    (range: VisibleRange) => {\n      const anchor = anchorOffset(\n        Option.fromNullishOr(\n          renderer?.getListScrollTop?.(instance.current?.id ?? -1),\n        ),\n        rowHeight,\n      );\n      rows.setScrollOffset(\n        Option.getOrElse(anchor, () => range.first * rowHeight),\n      );\n      onVisibleRange?.(range);\n    },\n    [renderer, rowHeight, rows, onVisibleRange],\n  );\n\n  useLayoutEffect(() => {\n    Option.match(rows.scrollTarget, {\n      onNone: () => {},\n      onSome: (index) => {\n        const list = instance.current;\n        if (list !== null) renderer?.scrollToItem?.(list.id, index);\n        rows.consumeScrollTarget();\n      },\n    });\n  }, [rows.scrollTarget, rows.consumeScrollTarget, renderer]);\n\n  return (\n    <VirtualList\n      {...rest}\n      ref={attach}\n      testId={testId}\n      height={Option.getOrUndefined(definite)}\n      itemCount={total}\n      estimatedItemHeight={rowHeight}\n      windowStart={rows.windowStart}\n      onVisibleRange={handleRange}\n      style={style}\n    >\n      {Arr.map(rows.mounted, (index) => (\n        <Fragment key={index}>{renderRow(index)}</Fragment>\n      ))}\n    </VirtualList>\n  );\n};\n\nexport interface VirtualTableProps extends Omit<\n  Props,\n  \"children\" | \"onVisibleRange\"\n> {\n  readonly count: number;\n\n  readonly height: number;\n\n  readonly rowHeight: number;\n\n  readonly header: ReactNode;\n\n  readonly row: (index: number) => ReactNode;\n\n  readonly empty?: ReactNode;\n\n  readonly caption?: ReactNode;\n\n  readonly overdraw?: number;\n\n  readonly reveal?: VirtualReveal;\n  readonly onVisibleRange?: (range: VisibleRange) => void;\n  readonly className?: string;\n}\n\nexport const VirtualTable = ({\n  count,\n  height,\n  rowHeight,\n  header,\n  row,\n  empty,\n  caption,\n  overdraw,\n  reveal,\n  onVisibleRange,\n  className,\n  style,\n  ...rest\n}: VirtualTableProps) => (\n  <Table\n    {...rest}\n    height={Option.getOrUndefined(paintableSize(height))}\n    className={className}\n    style={mergeStyles(paneFill(paintableSize(height)), style)}\n  >\n    <TableViewport>\n      <TableHeader>{header}</TableHeader>\n      {rowCount(count) === 0 ? (\n        <TableBody>\n          <TableEmpty>{emptyMessage(empty)}</TableEmpty>\n        </TableBody>\n      ) : (\n        <VirtualTableBody\n          count={count}\n          rowHeight={rowHeight}\n          renderRow={row}\n          overdraw={overdraw}\n          reveal={reveal}\n          onVisibleRange={onVisibleRange}\n        />\n      )}\n    </TableViewport>\n    {caption === undefined ? undefined : <TableCaption>{caption}</TableCaption>}\n  </Table>\n);\n\nexport type { VirtualRowsController };\n"
    },
    {
      "path": "packages/data/src/virtual/tree.tsx",
      "target": "@ui/gpuixui/data/virtual/tree.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport { useGpuix, type Props, type PublicInstance } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { rowHeightOf } from \"../../primitives/core/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport {\n  Fragment,\n  useCallback,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  type ReactNode,\n} from \"react\";\nimport { themeColors, useTheme } from \"../theme.js\";\nimport { recordsOf } from \"../internal/list.js\";\nimport { paintableSize } from \"../internal/size.js\";\nimport { paneFill, usePaintedHeight } from \"../internal/painted.js\";\nimport { treeName, type TreeCursor, type TreeRow } from \"../tree/logic.js\";\nimport { TreeRowView } from \"../tree/row.js\";\nimport { useTree } from \"../tree/use-tree.js\";\nimport { VirtualList, type VisibleRange } from \"./list.js\";\nimport { useVirtualRows } from \"./use-virtual-rows.js\";\nimport { anchorOffset, rowHeightOf as windowRowHeight } from \"./window.js\";\n\nexport interface VirtualTreeProps<T> extends Omit<\n  Props,\n  \"children\" | \"onVisibleRange\"\n> {\n  readonly rows: ReadonlyArray<TreeRow<T>>;\n\n  readonly labelOf: (node: T) => string;\n\n  readonly cursor?: TreeCursor;\n\n  readonly onCursorChange?: (cursor: TreeCursor) => void;\n\n  readonly defaultCursor?: TreeCursor;\n\n  readonly onActivate?: (row: TreeRow<T>) => void;\n\n  readonly renderRow?: (row: TreeRow<T>) => ReactNode;\n\n  readonly height: number;\n\n  readonly rowHeight?: number;\n\n  readonly overdraw?: number;\n\n  readonly onVisibleRange?: (range: VisibleRange) => void;\n\n  readonly instanceRef?: (instance: PublicInstance | null) => void;\n  readonly className?: string;\n\n  readonly testId?: string;\n}\n\nexport const VirtualTree = <T,>({\n  rows: dataRows,\n  labelOf,\n  cursor,\n  onCursorChange,\n  defaultCursor,\n  onActivate,\n  renderRow,\n  height,\n  rowHeight: namedRowHeight,\n  overdraw,\n  onVisibleRange,\n  onLayout,\n  instanceRef,\n  className,\n  testId,\n  style,\n  ...rest\n}: VirtualTreeProps<T>) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const renderer = useGpuix().renderer;\n\n  const bound = paintableSize(height);\n  const painted = usePaintedHeight(onLayout);\n\n  const rowHeight = windowRowHeight(namedRowHeight, rowHeightOf(theme));\n\n  const listHeight = Option.orElse(bound, () => painted.measured);\n\n  const rows = useMemo(() => recordsOf(dataRows), [dataRows]);\n  const window = useVirtualRows({\n    count: rows.length,\n    rowHeight,\n    viewportHeight: Option.getOrElse(listHeight, () => 0),\n    overdraw,\n  });\n  const tree = useTree({\n    rows,\n    labelOf,\n    cursor,\n    onCursorChange,\n    defaultCursor,\n    onActivate,\n  });\n  const instance = useRef<PublicInstance | null>(null);\n  const attach = useCallback(\n    (next: PublicInstance | null) => {\n      instance.current = next;\n      instanceRef?.(next);\n    },\n    [instanceRef],\n  );\n\n  const activeIndex = Option.flatMap(tree.cursor.active, (key) =>\n    Arr.findFirstIndex(rows, (row) => row.key === key),\n  );\n\n  Option.match(activeIndex, {\n    onNone: () => {},\n    onSome: (index) => {\n      if (!Arr.contains(window.mounted, index)) window.reveal(index);\n    },\n  });\n\n  useLayoutEffect(() => {\n    if (Option.isSome(activeIndex)) return;\n    if (rows.length === 0) return;\n    const index = Math.min(\n      rows.length - 1,\n      Math.max(0, Math.floor(window.scrollOffset / rowHeight)),\n    );\n    const row = rows[index];\n    if (row === undefined) return;\n    tree.commit(\n      { ...tree.cursor, active: Option.some(row.key) },\n      { focus: tree.holdsFocus() },\n    );\n  }, [activeIndex, rows, rowHeight, tree, window.scrollOffset]);\n\n  const handleRange = useCallback(\n    (range: VisibleRange) => {\n      const anchor = anchorOffset(\n        Option.fromNullishOr(\n          renderer?.getListScrollTop?.(instance.current?.id ?? -1),\n        ),\n        rowHeight,\n      );\n      const offset = Option.getOrElse(anchor, () => range.first * rowHeight);\n      window.setScrollOffset(offset);\n\n      Option.match(activeIndex, {\n        onNone: () => {},\n        onSome: (index) => {\n          if (Arr.contains(window.mountedAt(offset), index)) return;\n          const anchorRow =\n            rows[\n              Math.min(\n                rows.length - 1,\n                Math.max(0, Math.floor(offset / rowHeight)),\n              )\n            ];\n          if (anchorRow === undefined) return;\n          tree.commit(\n            { ...tree.cursor, active: Option.some(anchorRow.key) },\n            { focus: tree.holdsFocus() },\n          );\n        },\n      });\n      onVisibleRange?.(range);\n    },\n    [activeIndex, rows, onVisibleRange, renderer, rowHeight, tree, window],\n  );\n\n  useLayoutEffect(() => {\n    Option.match(window.scrollTarget, {\n      onNone: () => {},\n      onSome: (index) => {\n        const list = instance.current;\n        if (list !== null) renderer?.scrollToItem?.(list.id, index);\n        window.consumeScrollTarget();\n      },\n    });\n  }, [window.scrollTarget, window.consumeScrollTarget, renderer]);\n\n  const list = (\n    <VirtualList\n      {...rest}\n      ref={attach}\n      role=\"tree\"\n      testId={testId}\n      height={Option.getOrUndefined(listHeight)}\n      itemCount={rows.length}\n      estimatedItemHeight={rowHeight}\n      windowStart={window.windowStart}\n      onVisibleRange={handleRange}\n      className={className}\n      style={mergeStyles({ scrollbarColor: colors.scrollbar }, style)}\n    >\n      {Arr.map(window.mounted, (index) => {\n        const row = rows[index];\n        if (row === undefined) return null;\n        return (\n          <Fragment key={row.key}>\n            <TreeRowView\n              row={row}\n              active={Option.contains(tree.cursor.active, row.key)}\n              tabbable={Option.contains(tree.tabKey, row.key)}\n              label={treeName(labelOf(row.node), row.key)}\n              renderRow={renderRow}\n              height={rowHeight}\n              testId={\n                testId === undefined ? undefined : `${testId}-row-${row.key}`\n              }\n              onPress={() => tree.activate(row)}\n              onKeyDown={tree.handleKey}\n              attach={tree.attach(row.key)}\n            />\n          </Fragment>\n        );\n      })}\n    </VirtualList>\n  );\n\n  return Option.match(bound, {\n    onSome: () => list,\n    onNone: () => (\n      <div onLayout={painted.onLayout} style={paneFill(bound)}>\n        {list}\n      </div>\n    ),\n  });\n};\n"
    },
    {
      "path": "packages/data/src/virtual/use-virtual-rows.ts",
      "target": "@ui/gpuixui/data/virtual/use-virtual-rows.ts",
      "type": "registry:lib",
      "content": "import * as Option from \"effect/Option\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport {\n  clampScrollOffset,\n  createWindowCalculator,\n  rowCount,\n  rowHeightOf,\n  rowIndex,\n  type VirtualWindow,\n  type WindowSpec,\n} from \"./window.js\";\n\nexport interface VirtualReveal {\n  readonly index: number;\n}\n\nexport interface UseVirtualRowsOptions extends WindowSpec {\n  readonly initialOffset?: number;\n\n  readonly reveal?: VirtualReveal;\n}\n\nexport interface VirtualRowsController {\n  readonly window: VirtualWindow;\n\n  readonly windowStart: number;\n\n  readonly mounted: ReadonlyArray<number>;\n\n  readonly mountedAt: (offset: number) => ReadonlyArray<number>;\n\n  readonly scrollOffset: number;\n\n  readonly setScrollOffset: (offset: number) => void;\n\n  readonly reveal: (index: number) => VirtualWindow;\n\n  readonly scrollTarget: Option.Option<number>;\n  readonly consumeScrollTarget: () => void;\n}\n\nexport const useVirtualRows = ({\n  count,\n  rowHeight,\n  viewportHeight,\n  overdraw,\n  initialOffset,\n  reveal,\n}: UseVirtualRowsOptions): VirtualRowsController => {\n  const height = rowHeightOf(rowHeight);\n  const calculator = useMemo(\n    () =>\n      createWindowCalculator({\n        count,\n        rowHeight: height,\n        viewportHeight,\n        overdraw,\n      }),\n    [count, height, viewportHeight, overdraw],\n  );\n  const [scrollOffset, setScrollOffset] = useState(initialOffset ?? 0);\n  const [lastReveal, setLastReveal] = useState<VirtualReveal | undefined>(\n    undefined,\n  );\n  const [scrollTarget, setScrollTarget] = useState<Option.Option<number>>(\n    Option.none(),\n  );\n\n  const spec = { count, rowHeight: height, viewportHeight, overdraw };\n  const offset = clampScrollOffset(spec, scrollOffset);\n\n  if (offset !== scrollOffset) setScrollOffset(offset);\n\n  const window = calculator.at(offset);\n  const mounted = calculator.indicesAt(offset);\n\n  const revealTo = useCallback(\n    (index: number) => {\n      const target = calculator.around(index);\n\n      setScrollOffset(rowIndex(index) * height);\n      return target;\n    },\n    [calculator, height],\n  );\n\n  if (reveal !== undefined && reveal !== lastReveal) {\n    setLastReveal(reveal);\n    revealTo(reveal.index);\n\n    const row = Math.min(rowCount(count) - 1, rowIndex(reveal.index));\n    if (row >= 0) setScrollTarget(Option.some(row));\n  }\n\n  return {\n    window,\n    windowStart: window.start,\n    mounted,\n    mountedAt: calculator.indicesAt,\n    scrollOffset: offset,\n    setScrollOffset,\n    reveal: revealTo,\n    scrollTarget,\n    consumeScrollTarget: useCallback(() => setScrollTarget(Option.none()), []),\n  };\n};\n"
    },
    {
      "path": "packages/data/src/virtual/window.ts",
      "target": "@ui/gpuixui/data/virtual/window.ts",
      "type": "registry:lib",
      "content": "import { Virtualizer } from \"@tanstack/virtual-core\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\n\nexport const DEFAULT_OVERDRAW = 4;\n\nexport const DEFAULT_ROW_HEIGHT = 37;\n\nexport interface VirtualWindow {\n  readonly start: number;\n  readonly end: number;\n}\n\nexport interface WindowSpec {\n  readonly count: number;\n\n  readonly rowHeight: number;\n\n  readonly viewportHeight: number;\n\n  readonly overdraw?: number;\n}\n\nexport interface WindowCalculator {\n  readonly indicesAt: (scrollOffset: number) => ReadonlyArray<number>;\n\n  readonly at: (scrollOffset: number) => VirtualWindow;\n\n  readonly around: (index: number) => VirtualWindow;\n}\n\nexport const rowHeightOf = (\n  height: number | undefined,\n  fallback: number = DEFAULT_ROW_HEIGHT,\n): number =>\n  height !== undefined && Number.isFinite(height) && height > 0\n    ? height\n    : fallback;\n\nconst viewportOf = (spec: WindowSpec): number =>\n  Number.isFinite(spec.viewportHeight) && spec.viewportHeight > 0\n    ? spec.viewportHeight\n    : rowHeightOf(spec.rowHeight);\n\nexport const overdrawRows = (overdraw: number | undefined): number =>\n  overdraw === undefined || !Number.isFinite(overdraw)\n    ? DEFAULT_OVERDRAW\n    : Math.max(0, Math.floor(overdraw));\n\nexport const rowCount = (count: number): number =>\n  Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0;\n\nexport const maxScrollOffset = (spec: WindowSpec): number =>\n  Math.max(\n    0,\n    rowCount(spec.count) * rowHeightOf(spec.rowHeight) - viewportOf(spec),\n  );\n\nexport const clampScrollOffset = (\n  spec: WindowSpec,\n  scrollOffset: number,\n): number => {\n  const offset = Number.isFinite(scrollOffset) ? Math.max(0, scrollOffset) : 0;\n  return Math.min(offset, maxScrollOffset(spec));\n};\n\nexport const createWindowCalculator = (spec: WindowSpec): WindowCalculator => {\n  const rowHeight = rowHeightOf(spec.rowHeight);\n  const virtualizer = new Virtualizer<never, never>({\n    count: rowCount(spec.count),\n    estimateSize: () => rowHeight,\n    getScrollElement: () => null,\n    observeElementRect: () => undefined,\n    observeElementOffset: () => undefined,\n    scrollToFn: () => undefined,\n    overscan: overdrawRows(spec.overdraw),\n  });\n\n  const indicesAt = (scrollOffset: number): ReadonlyArray<number> => {\n    virtualizer.scrollRect = { width: 0, height: viewportOf(spec) };\n    virtualizer.scrollOffset = clampScrollOffset(spec, scrollOffset);\n    return Arr.filter(\n      virtualizer.getVirtualIndexes(),\n      (index) => index >= 0 && index < rowCount(spec.count),\n    );\n  };\n\n  const at = (scrollOffset: number): VirtualWindow => {\n    const indices = indicesAt(scrollOffset);\n    const first = Option.getOrUndefined(Arr.head(indices));\n    const last = Option.getOrUndefined(Arr.last(indices));\n    return first === undefined || last === undefined\n      ? { start: 0, end: 0 }\n      : { start: first, end: last + 1 };\n  };\n\n  return {\n    indicesAt,\n    at,\n    around: (index) => at(rowIndex(index) * rowHeight),\n  };\n};\n\nexport const rowIndex = (index: number): number =>\n  Number.isFinite(index) ? Math.max(0, Math.floor(index)) : 0;\n\nexport const anchorOffset = (\n  anchor: Option.Option<ReadonlyArray<number>>,\n  rowHeight: number,\n): Option.Option<number> =>\n  Option.flatMap(anchor, (values) => {\n    const index = values[0];\n    const offsetInItem = values[1];\n    if (index === undefined || offsetInItem === undefined) return Option.none();\n    const into = Number.isFinite(offsetInItem) ? Math.max(0, offsetInItem) : 0;\n    return Option.some(rowIndex(index) * rowHeightOf(rowHeight) + into);\n  });\n"
    }
  ],
  "docs": "[Runtime: data](https://gpuixui.sudokw.com/registry) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/registry",
    "kind": "runtime",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}