shadimage
openmirai
blockcalculatorsource: ok · hit

Mirai Scientific Calculator

An editable scientific, graphing, statistics, and math tools calculator.

npx shadcn add "https://www.openmirai.dev/r/calculator.json"
free

Where it goes

statsdata-table

Interaction

animated-counter

Use cases

dashboard

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@openmirai/calculator-core@^0.2.0lucide-react@^1.28.0

Registry dependencies

badgebuttoncardinputscroll-areaselectseparatorswitchtabstextareatooltip

Files22 · source cached 10/8/2026, 3:13:07 PM

../../packages/calculator-registry/src/components/mirai-calculator/calculator-empty-state-action.tsx
interface CalculatorEmptyStateActionProps {
  label: string
  onClick: () => void
}

/** Renders the inline Add action used by calculator empty-state guidance. */
export function CalculatorEmptyStateAction({ label, onClick }: CalculatorEmptyStateActionProps) {
  return (
    <button
      type="button"
      aria-label={label}
      onClick={onClick}
      className="font-semibold text-foreground underline-offset-4 hover:underline focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
    >
      Add
    </button>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/calculator-extension-definitions.tsx
import { GraphingMode } from "@/components/mirai-calculator/modes/graphing-mode"
import { ScientificMode } from "@/components/mirai-calculator/modes/scientific-mode"
import { StatisticsMode } from "@/components/mirai-calculator/modes/statistics-mode"
import { ToolsMode } from "@/components/mirai-calculator/modes/tools-mode"
import { CalculatorExtension } from "@openmirai/calculator-core/configuration"
import type {
  CalculatorEngine,
  AngleMode,
  NumberFormatOptions,
} from "@openmirai/calculator-core/engine"
import type { GraphingInitialData } from "@openmirai/calculator-core/graphing-data"
import type { GraphView } from "@openmirai/calculator-core/graphing-view"
import type { StatisticsInitialData } from "@openmirai/calculator-core/statistics-data"
import type { ToolsInitialData } from "@openmirai/calculator-core/tools"

interface ModeRendererProps {
  resetSignal: number
  engine: CalculatorEngine
  activeAngleMode: AngleMode
  ans: number
  definitions: Array<string>
  setDefinitions: (definitions: Array<string>) => void
  setAns: (ans: number) => void
  formatOptions: NumberFormatOptions
  formatNumber: (value: number) => string
  setVariables: (variables: Record<string, number>) => void
  graphView: GraphView
  setGraphView: (view: GraphView) => void
  graphGridVisible: boolean
  setGraphGridVisible: (visible: boolean) => void
  resolvedTheme: "light" | "dark"
  defaultGraphingData?: GraphingInitialData
  defaultStatisticsData?: StatisticsInitialData
  defaultToolsData?: ToolsInitialData
}

interface ExtensionDefinition {
  label: string
  compactLabel: string
  usesAngleMode: boolean
  hasGraphSettings: boolean
  render: (props: ModeRendererProps) => React.ReactNode
}

export const CALCULATOR_EXTENSION_DEFINITIONS: Record<CalculatorExtension, ExtensionDefinition> = {
  [CalculatorExtension.SCIENTIFIC]: {
    label: "Scientific",
    compactLabel: "Sci",
    usesAngleMode: true,
    hasGraphSettings: false,
    render: ({ resetSignal, engine, definitions, setDefinitions, setAns, formatOptions }) => (
      <ScientificMode
        key={`scientific-${resetSignal}`}
        engine={engine}
        definitions={definitions}
        onDefinitionsChange={setDefinitions}
        onAnsChange={setAns}
        formatOptions={formatOptions}
      />
    ),
  },
  [CalculatorExtension.GRAPHING]: {
    label: "Graphing",
    compactLabel: "Graph",
    usesAngleMode: true,
    hasGraphSettings: true,
    render: ({
      resetSignal,
      activeAngleMode,
      ans,
      definitions,
      formatNumber,
      setVariables,
      graphView,
      setGraphView,
      graphGridVisible,
      setGraphGridVisible,
      resolvedTheme,
      defaultGraphingData,
    }) => (
      <GraphingMode
        key={`graphing-${resetSignal}`}
        angleMode={activeAngleMode}
        ans={ans}
        definitions={definitions}
        formatNumber={formatNumber}
        onVariablesChange={setVariables}
        view={graphView}
        onViewChange={setGraphView}
        gridVisible={graphGridVisible}
        onGridVisibleChange={setGraphGridVisible}
        colorScheme={resolvedTheme}
        defaultData={defaultGraphingData}
      />
    ),
  },
  [CalculatorExtension.STATISTICS]: {
    label: "Stats",
    compactLabel: "Stats",
    usesAngleMode: false,
    hasGraphSettings: false,
    render: ({ resetSignal, formatNumber, resolvedTheme, defaultStatisticsData }) => (
      <StatisticsMode
        key={`statistics-${resetSignal}`}
        formatNumber={formatNumber}
        colorScheme={resolvedTheme}
        defaultData={defaultStatisticsData}
      />
    ),
  },
  [CalculatorExtension.TOOLS]: {
    label: "Tools",
    compactLabel: "Tools",
    usesAngleMode: false,
    hasGraphSettings: false,
    render: ({ resetSignal, formatNumber, defaultToolsData }) => (
      <ToolsMode
        key={`tools-${resetSignal}`}
        formatNumber={formatNumber}
        defaultData={defaultToolsData}
      />
    ),
  },
}
../../packages/calculator-registry/src/components/mirai-calculator/calculator-header-icon-button.tsx
import { buttonVariants } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"

/** Renders a compact calculator header control with a themed tooltip. */
export function CalculatorHeaderIconButton({
  label,
  children,
  onClick,
  portalTheme,
}: {
  label: string
  children: React.ReactNode
  onClick: () => void
  portalTheme: "light" | "dark"
}) {
  return (
    <Tooltip>
      <TooltipTrigger
        onClick={onClick}
        aria-label={label}
        className={cn(buttonVariants({ variant: "outline", size: "icon" }), "size-7.5 rounded-md")}
      >
        {children}
      </TooltipTrigger>
      <TooltipContent
        className={cn("mirai-calculator-package mirai-calculator-portal z-2147483002", portalTheme)}
        data-theme={portalTheme}
      >
        {label}
      </TooltipContent>
    </Tooltip>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/calculator-settings-dialog.tsx
import { useEffect } from "react"
import { createPortal } from "react-dom"
import { Settings2, X } from "lucide-react"

import {
  CALCULATOR_ANGLE_MODE_LABELS,
  CALCULATOR_DECIMAL_OPTIONS,
  CALCULATOR_GRAPH_BOUNDARY_KEYS,
  CALCULATOR_NOTATION_LABELS,
  CALCULATOR_THEME_LABELS,
} from "@/components/mirai-calculator/calculator-ui-config"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import type {
  CalculatorDisplaySettings,
  CalculatorTheme,
} from "@openmirai/calculator-core/configuration"
import type { AngleMode } from "@openmirai/calculator-core/engine"
import type { GraphView } from "@openmirai/calculator-core/graphing-view"
import { cn } from "@/lib/utils"

interface CalculatorSettingsDialogProps {
  settingsOpen: boolean
  calculatorContainer: HTMLDivElement | null
  setSettingsOpen: React.Dispatch<React.SetStateAction<boolean>>
  resolvedTheme: "light" | "dark"
  showsAngleMode: boolean
  activeAngleMode: AngleMode
  setAngleMode: (mode: AngleMode) => void
  displaySettings: CalculatorDisplaySettings
  setDisplaySettings: React.Dispatch<React.SetStateAction<CalculatorDisplaySettings>>
  showsGraphSettings: boolean
  graphGridVisible: boolean
  setGraphGridVisible: React.Dispatch<React.SetStateAction<boolean>>
  graphView: GraphView
  updateGraphBoundary: (key: keyof GraphView, source: string) => void
  activeTheme: CalculatorTheme
  setTheme: (theme: CalculatorTheme) => void
}

/** Renders calculator settings inside the calculator container rather than the host page. */
export function CalculatorSettingsDialog({
  settingsOpen,
  calculatorContainer,
  setSettingsOpen,
  resolvedTheme,
  showsAngleMode,
  activeAngleMode,
  setAngleMode,
  displaySettings,
  setDisplaySettings,
  showsGraphSettings,
  graphGridVisible,
  setGraphGridVisible,
  graphView,
  updateGraphBoundary,
  activeTheme,
  setTheme,
}: CalculatorSettingsDialogProps) {
  useEffect(() => {
    if (!settingsOpen) {
      return undefined
    }

    const closeOnEscape = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        setSettingsOpen(false)
      }
    }
    document.addEventListener("keydown", closeOnEscape)
    return () => document.removeEventListener("keydown", closeOnEscape)
  }, [settingsOpen, setSettingsOpen])

  return (
    <>
      {settingsOpen && calculatorContainer
        ? createPortal(
            <div
              role="presentation"
              className="absolute inset-0 z-60 flex items-center justify-center overflow-hidden rounded-[inherit] bg-black/25 p-3 backdrop-blur-[7px]"
              onPointerDown={(event) => {
                if (event.target === event.currentTarget) {
                  setSettingsOpen(false)
                }
              }}
            >
              <section
                role="dialog"
                aria-modal="true"
                aria-label="Calculator settings"
                tabIndex={-1}
                className={cn(
                  "mirai-calculator-package mirai-calculator-portal mirai-calculator-settings-dialog relative grid min-h-0 max-h-[calc(100%-1.5rem)] w-[calc(100%-1.5rem)] max-w-160 grid-rows-[auto_minmax(0,1fr)_auto] gap-0 overflow-hidden rounded-xl border bg-popover p-0 shadow-2xl ring-1 ring-foreground/10",
                  resolvedTheme
                )}
                data-theme={resolvedTheme}
              >
                <Button
                  type="button"
                  aria-label="Close calculator settings"
                  variant="ghost"
                  size="icon-sm"
                  onClick={() => setSettingsOpen(false)}
                  className="absolute top-2 right-2 z-10"
                >
                  <X />
                </Button>
                <div className="border-b bg-muted/20 px-5 py-4 pr-12">
                  <div className="flex items-center gap-3">
                    <span className="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-background text-muted-foreground shadow-xs">
                      <Settings2 className="size-4" />
                    </span>
                    <div className="min-w-0">
                      <h2 className="text-sm font-semibold">Calculator settings</h2>
                      <p className="mt-1 text-xs text-muted-foreground">
                        Format results and tune the active workspace.
                      </p>
                    </div>
                  </div>
                </div>

                <div
                  data-calculator-settings-body=""
                  className="grid min-h-0 gap-4 overflow-y-auto overscroll-contain p-5 sm:grid-cols-2"
                >
                  <section
                    aria-labelledby="mirai-number-format-heading"
                    className="rounded-lg border bg-background/60 p-4"
                  >
                    <h3
                      id="mirai-number-format-heading"
                      className="mb-2 text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground"
                    >
                      Number format
                    </h3>
                    <div className="grid grid-cols-2 gap-3">
                      {showsAngleMode && (
                        <div className="grid gap-1.5 text-xs font-medium text-muted-foreground">
                          <span>Angle mode</span>
                          <Select
                            value={activeAngleMode}
                            onValueChange={(value: string | null) => {
                              if (value === "degrees" || value === "radians") {
                                setAngleMode(value)
                              }
                            }}
                          >
                            <SelectTrigger
                              size="sm"
                              className="w-full bg-background"
                              aria-label="Angle mode"
                            >
                              <SelectValue>
                                {CALCULATOR_ANGLE_MODE_LABELS[activeAngleMode]}
                              </SelectValue>
                            </SelectTrigger>
                            <SelectContent
                              className={cn(
                                "mirai-calculator-package mirai-calculator-portal z-2147483002",
                                resolvedTheme
                              )}
                              data-theme={resolvedTheme}
                            >
                              <SelectItem value="degrees">Degrees</SelectItem>
                              <SelectItem value="radians">Radians</SelectItem>
                            </SelectContent>
                          </Select>
                        </div>
                      )}

                      <div className="grid gap-1.5 text-xs font-medium text-muted-foreground">
                        <span>Notation</span>
                        <Select
                          value={displaySettings.notation}
                          onValueChange={(value: string | null) => {
                            if (value !== "auto" && value !== "scientific") {
                              return
                            }
                            setDisplaySettings((current) => ({
                              ...current,
                              notation: value,
                            }))
                          }}
                        >
                          <SelectTrigger
                            size="sm"
                            className="w-full bg-background"
                            aria-label="Notation"
                          >
                            <SelectValue>
                              {CALCULATOR_NOTATION_LABELS[displaySettings.notation]}
                            </SelectValue>
                          </SelectTrigger>
                          <SelectContent
                            className={cn(
                              "mirai-calculator-package mirai-calculator-portal z-2147483002",
                              resolvedTheme
                            )}
                            data-theme={resolvedTheme}
                          >
                            <SelectItem value="auto">Automatic</SelectItem>
                            <SelectItem value="scientific">Scientific</SelectItem>
                          </SelectContent>
                        </Select>
                      </div>

                      <div className="grid gap-1.5 text-xs font-medium text-muted-foreground">
                        <span>Decimals</span>
                        <Select
                          value={String(displaySettings.decimals)}
                          onValueChange={(value) => {
                            if (value === null) {
                              return
                            }
                            setDisplaySettings((current) => ({
                              ...current,
                              decimals: value === "auto" ? "auto" : Number(value),
                            }))
                          }}
                        >
                          <SelectTrigger
                            size="sm"
                            className="w-full bg-background"
                            aria-label="Decimals"
                          >
                            <SelectValue>
                              {displaySettings.decimals === "auto"
                                ? "Auto"
                                : String(displaySettings.decimals)}
                            </SelectValue>
                          </SelectTrigger>
                          <SelectContent
                            className={cn(
                              "mirai-calculator-package mirai-calculator-portal z-2147483002",
                              resolvedTheme
                            )}
                            data-theme={resolvedTheme}
                          >
                            <SelectItem value="auto">Auto</SelectItem>
                            {CALCULATOR_DECIMAL_OPTIONS.map((value) => (
                              <SelectItem key={value} value={String(value)}>
                                {value}
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </Select>
                      </div>

                      <label
                        htmlFor="mirai-significant-figures"
                        className="grid gap-1.5 text-xs font-medium text-muted-foreground"
                      >
                        Significant figures
                        <Input
                          id="mirai-significant-figures"
                          type="number"
                          min={1}
                          max={15}
                          value={displaySettings.significantFigures}
                          onChange={(event) =>
                            setDisplaySettings((current) => ({
                              ...current,
                              significantFigures: Math.max(
                                1,
                                Math.min(15, Number(event.target.value) || 1)
                              ),
                            }))
                          }
                          className="h-7 w-full bg-background"
                        />
                      </label>
                    </div>

                    <div className="mt-3 flex items-center justify-between gap-4 rounded-md border bg-muted/25 px-3 py-2.5 text-sm">
                      <span>
                        <strong className="block text-xs font-medium">Thousands separators</strong>
                        <small className="text-xs text-muted-foreground">
                          Group large values for readability.
                        </small>
                      </span>
                      <Switch
                        aria-label="Thousands separators"
                        checked={displaySettings.thousandsSeparator}
                        onCheckedChange={(checked) =>
                          setDisplaySettings((current) => ({
                            ...current,
                            thousandsSeparator: checked,
                          }))
                        }
                      />
                    </div>
                  </section>

                  {showsGraphSettings && (
                    <section
                      aria-labelledby="mirai-graph-viewport-heading"
                      className="rounded-lg border bg-background/60 p-4"
                    >
                      <div className="mb-2 flex items-center justify-between gap-3">
                        <h3
                          id="mirai-graph-viewport-heading"
                          className="text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground"
                        >
                          Graph viewport
                        </h3>
                        <div className="flex items-center gap-2 text-xs">
                          <span>Grid lines</span>
                          <Switch
                            aria-label="Grid lines"
                            checked={graphGridVisible}
                            onCheckedChange={setGraphGridVisible}
                          />
                        </div>
                      </div>
                      <div className="grid grid-cols-2 gap-3">
                        {CALCULATOR_GRAPH_BOUNDARY_KEYS.map((key) => (
                          <label
                            key={key}
                            htmlFor={`mirai-graph-${key}`}
                            className="grid gap-1.5 text-xs font-medium text-muted-foreground"
                          >
                            {key.replace("min", " min").replace("max", " max")}
                            <Input
                              id={`mirai-graph-${key}`}
                              type="number"
                              value={graphView[key]}
                              onChange={(event) => updateGraphBoundary(key, event.target.value)}
                              className="h-7 w-full bg-background font-mono"
                            />
                          </label>
                        ))}
                      </div>
                    </section>
                  )}

                  <section
                    aria-labelledby="mirai-appearance-heading"
                    className="rounded-lg border bg-background/60 p-4"
                  >
                    <h3
                      id="mirai-appearance-heading"
                      className="mb-2 text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground"
                    >
                      Appearance
                    </h3>
                    <div className="grid gap-1.5 text-xs font-medium text-muted-foreground">
                      <span>Theme</span>
                      <Select
                        value={activeTheme}
                        onValueChange={(value: string | null) => {
                          if (value === "light" || value === "dark" || value === "system") {
                            setTheme(value)
                          }
                        }}
                      >
                        <SelectTrigger
                          size="sm"
                          className="w-full bg-background"
                          aria-label="Theme"
                        >
                          <SelectValue>{CALCULATOR_THEME_LABELS[activeTheme]}</SelectValue>
                        </SelectTrigger>
                        <SelectContent
                          className={cn(
                            "mirai-calculator-package mirai-calculator-portal z-2147483002",
                            resolvedTheme
                          )}
                          data-theme={resolvedTheme}
                        >
                          <SelectItem value="light">Light</SelectItem>
                          <SelectItem value="dark">Dark</SelectItem>
                          <SelectItem value="system">System</SelectItem>
                        </SelectContent>
                      </Select>
                    </div>
                  </section>
                </div>
                <div className="flex justify-end border-t bg-muted/20 px-5 py-3">
                  <Button variant="outline" size="sm" onClick={() => setSettingsOpen(false)}>
                    Done
                  </Button>
                </div>
              </section>
            </div>,
            calculatorContainer
          )
        : null}
    </>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/calculator-ui-config.ts
/** Supported decimal-place options displayed by calculator settings. */
export const CALCULATOR_DECIMAL_OPTIONS = [0, 1, 2, 3, 4, 6] as const

/** Graph viewport properties exposed by calculator settings. */
export const CALCULATOR_GRAPH_BOUNDARY_KEYS = ["xmin", "xmax", "ymin", "ymax"] as const

/** Human-readable angle-mode labels. */
export const CALCULATOR_ANGLE_MODE_LABELS = {
  degrees: "Degrees",
  radians: "Radians",
} as const

/** Human-readable number-notation labels. */
export const CALCULATOR_NOTATION_LABELS = {
  auto: "Automatic",
  scientific: "Scientific",
} as const

/** Human-readable calculator theme labels. */
export const CALCULATOR_THEME_LABELS = {
  dark: "Dark",
  light: "Light",
  system: "System",
} as const

/** Semantic shadcn chart tokens used by calculator visualizations. */
export const CALCULATOR_CHART_TOKENS = [
  "var(--chart-1)",
  "var(--chart-2)",
  "var(--chart-3)",
  "var(--chart-4)",
  "var(--chart-5)",
] as const
../../packages/calculator-registry/src/components/mirai-calculator/calculator-viewport-portal.tsx
import { useLayoutEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"

/** Moves calculator content between its inline host and the fullscreen viewport layer. */
export function CalculatorViewportPortal({
  fullscreen,
  children,
}: {
  fullscreen: boolean
  children: React.ReactNode
}) {
  const inlineContainerRef = useRef<HTMLDivElement>(null)
  const [portalHost, setPortalHost] = useState<HTMLDivElement | null>(null)

  useLayoutEffect(() => {
    const host = document.createElement("div")
    host.style.display = "contents"
    setPortalHost(host)
    return () => host.remove()
  }, [])

  useLayoutEffect(() => {
    if (!portalHost) {
      return
    }
    const target = fullscreen ? document.body : inlineContainerRef.current
    target?.append(portalHost)
  }, [fullscreen, portalHost])

  return (
    <div ref={inlineContainerRef} style={{ display: "contents" }}>
      {portalHost
        ? createPortal(
            <>
              {fullscreen && (
                <div
                  aria-hidden="true"
                  data-mirai-fullscreen-guard=""
                  className="fixed inset-0 z-2147482999 bg-black/35 backdrop-blur-[1px]"
                />
              )}
              {children}
            </>,
            portalHost
          )
        : children}
    </div>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/canvas-theme.ts
import { CALCULATOR_CHART_TOKENS } from "@/components/mirai-calculator/calculator-ui-config"

/** Reads a shadcn color property with a computed foreground or background fallback. */
export function readCanvasColor(element: Element, property: string): string {
  const computed = getComputedStyle(element)
  const value = computed.getPropertyValue(property).trim()
  return value || (property === "--background" ? computed.backgroundColor : computed.color)
}

/** Resolves a CSS custom-property token to a canvas-compatible color. */
export function resolveCanvasColor(element: Element, token: string): string {
  const computed = getComputedStyle(element)
  const match = /^var\((--[^)]+)\)$/.exec(token.trim())
  if (!match) {
    return token || computed.color
  }
  return computed.getPropertyValue(match[1]).trim() || computed.color
}

/** Returns every configured shadcn chart color resolved for a canvas element. */
export function readCanvasChartColors(element: Element): Array<string> {
  return CALCULATOR_CHART_TOKENS.map((token) => resolveCanvasColor(element, token))
}
../../packages/calculator-registry/src/components/mirai-calculator/mirai-calculator.tsx
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import {
  Calculator as CalculatorIcon,
  Expand,
  EyeOff,
  Minimize2,
  Moon,
  RotateCcw,
  Settings2,
  Sun,
  X,
} from "lucide-react"

import { CalculatorHeaderIconButton } from "@/components/mirai-calculator/calculator-header-icon-button"
import { CALCULATOR_EXTENSION_DEFINITIONS } from "@/components/mirai-calculator/calculator-extension-definitions"
import { CalculatorSettingsDialog } from "@/components/mirai-calculator/calculator-settings-dialog"
import { CalculatorViewportPortal } from "@/components/mirai-calculator/calculator-viewport-portal"
import {
  resolveCalculatorTheme,
  useSystemDarkMode,
} from "@/components/mirai-calculator/use-system-dark-mode"
import { Button, buttonVariants } from "@/components/ui/button"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import {
  CalculatorExtension,
  DEFAULT_DISPLAY_SETTINGS,
  DEFAULT_GRAPH_VIEW,
  EMPTY_CALCULATOR_DEFINITIONS,
  calculatorNumberFormatOptions,
  collectSliderVariables,
  normalizeCalculatorExtensions,
} from "@openmirai/calculator-core/configuration"
import type {
  CalculatorDisplaySettings,
  CalculatorMode,
  CalculatorTheme,
} from "@openmirai/calculator-core/configuration"
import { CalculatorEngine } from "@openmirai/calculator-core/engine"
import type { AngleMode, NumberFormatOptions } from "@openmirai/calculator-core/engine"
import type { GraphingInitialData } from "@openmirai/calculator-core/graphing-data"
import type { GraphView } from "@openmirai/calculator-core/graphing-view"
import type { StatisticsInitialData } from "@openmirai/calculator-core/statistics-data"
import type { ToolsInitialData as ToolsInitialDataValue } from "@openmirai/calculator-core/tools"
import { cn } from "@/lib/utils"

export interface MiraiCalculatorProps {
  className?: string
  extensions?: ReadonlyArray<CalculatorExtension>
  mode?: CalculatorMode
  defaultMode?: CalculatorMode
  onModeChange?: (mode: CalculatorMode) => void
  angleMode?: AngleMode
  defaultAngleMode?: AngleMode
  onAngleModeChange?: (mode: AngleMode) => void
  theme?: CalculatorTheme
  defaultTheme?: CalculatorTheme
  onThemeChange?: (theme: CalculatorTheme) => void
  hidden?: boolean
  defaultHidden?: boolean
  onHiddenChange?: (hidden: boolean) => void
  startFullscreen?: boolean
  title?: string
  onClose?: () => void
  defaultDefinitions?: ReadonlyArray<string>
  defaultGraphingData?: GraphingInitialData
  defaultStatisticsData?: StatisticsInitialData
  defaultToolsData?: ToolsInitialDataValue
}

/** Composes enabled calculator extensions into a configurable, container-responsive panel. */
export function MiraiCalculator({
  className,
  extensions,
  mode,
  defaultMode = CalculatorExtension.SCIENTIFIC,
  onModeChange,
  angleMode,
  defaultAngleMode = "degrees",
  onAngleModeChange,
  theme,
  defaultTheme = "light",
  onThemeChange,
  hidden,
  defaultHidden = false,
  onHiddenChange,
  startFullscreen = false,
  title = "Calculator",
  onClose,
  defaultDefinitions = EMPTY_CALCULATOR_DEFINITIONS,
  defaultGraphingData,
  defaultStatisticsData,
  defaultToolsData,
}: MiraiCalculatorProps) {
  const [internalMode, setInternalMode] = useState(defaultMode)
  const [internalAngleMode, setInternalAngleMode] = useState(defaultAngleMode)
  const [internalTheme, setInternalTheme] = useState(defaultTheme)
  const [internalHidden, setInternalHidden] = useState(defaultHidden)
  const systemDark = useSystemDarkMode()
  const [fullscreen, setFullscreen] = useState(startFullscreen)
  const [settingsOpen, setSettingsOpen] = useState(false)
  const [displaySettings, setDisplaySettings] = useState<CalculatorDisplaySettings>(() => ({
    ...DEFAULT_DISPLAY_SETTINGS,
  }))
  const [graphView, setGraphView] = useState<GraphView>(() => ({ ...DEFAULT_GRAPH_VIEW }))
  const [graphGridVisible, setGraphGridVisible] = useState(true)
  const [definitions, setDefinitions] = useState(() => [...defaultDefinitions])
  const [variables, setVariables] = useState<Record<string, number>>(() =>
    collectSliderVariables(
      (defaultGraphingData?.expressions ?? []).map((expression) => ({
        expression: expression.value,
        value: expression.slider?.value,
      }))
    )
  )
  const [ans, setAns] = useState(0)
  const [resetSignal, setResetSignal] = useState(0)
  const calculatorRef = useRef<HTMLDivElement>(null)

  const enabledExtensions = useMemo(() => normalizeCalculatorExtensions(extensions), [extensions])
  const requestedMode = mode ?? internalMode
  const activeMode = enabledExtensions.includes(requestedMode)
    ? requestedMode
    : enabledExtensions[0]
  const activeAngleMode = angleMode ?? internalAngleMode
  const activeTheme = theme ?? internalTheme
  const activeHidden = hidden ?? internalHidden
  const resolvedTheme = resolveCalculatorTheme(activeTheme, systemDark)
  const showsAngleMode = enabledExtensions.some(
    (extension) => CALCULATOR_EXTENSION_DEFINITIONS[extension].usesAngleMode
  )
  const showsGraphSettings = enabledExtensions.some(
    (extension) => CALCULATOR_EXTENSION_DEFINITIONS[extension].hasGraphSettings
  )

  useEffect(() => {
    if (!fullscreen) {
      return
    }
    const previousOverflow = document.documentElement.style.overflow
    document.documentElement.style.overflow = "hidden"
    return () => {
      document.documentElement.style.overflow = previousOverflow
    }
  }, [fullscreen])

  const setMode = (nextMode: CalculatorMode) => {
    if (mode === undefined) {
      setInternalMode(nextMode)
    }
    onModeChange?.(nextMode)
  }

  const setAngleMode = (nextMode: AngleMode) => {
    if (angleMode === undefined) {
      setInternalAngleMode(nextMode)
    }
    onAngleModeChange?.(nextMode)
  }

  const setTheme = (nextTheme: CalculatorTheme) => {
    if (theme === undefined) {
      setInternalTheme(nextTheme)
    }
    onThemeChange?.(nextTheme)
  }

  const setHidden = (nextHidden: boolean) => {
    if (hidden === undefined) {
      setInternalHidden(nextHidden)
    }
    onHiddenChange?.(nextHidden)
    if (nextHidden) {
      setFullscreen(false)
      setSettingsOpen(false)
    }
  }

  const formatOptions = useMemo<NumberFormatOptions>(
    () => calculatorNumberFormatOptions(displaySettings),
    [displaySettings]
  )

  const engine = useMemo(
    () =>
      new CalculatorEngine({
        angleMode: activeAngleMode,
        ans,
        definitions,
        variables,
      }),
    [activeAngleMode, ans, definitions, variables]
  )

  const formatNumber = useCallback(
    (value: number) => engine.format(value, formatOptions),
    [engine, formatOptions]
  )

  const reset = () => {
    setDisplaySettings({ ...DEFAULT_DISPLAY_SETTINGS })
    setGraphView({ ...DEFAULT_GRAPH_VIEW })
    setGraphGridVisible(true)
    setDefinitions([...defaultDefinitions])
    setVariables({})
    setAns(0)
    setResetSignal((signal) => signal + 1)
  }

  const updateGraphBoundary = (key: keyof GraphView, source: string) => {
    const value = Number(source)
    if (!Number.isFinite(value)) {
      return
    }
    setGraphView((current) => {
      const next = { ...current, [key]: value }
      if (next.xmin >= next.xmax || next.ymin >= next.ymax) {
        return current
      }
      return next
    })
  }

  return (
    <TooltipProvider>
      <div className="mirai-calculator-package" style={{ display: "contents" }}>
        <div className="relative isolate h-full min-h-0 w-full">
          {activeHidden && (
            <Tooltip>
              <TooltipTrigger
                type="button"
                aria-label="Show calculator"
                onClick={() => setHidden(false)}
                className={cn(
                  buttonVariants({ variant: "default", size: "icon" }),
                  "mirai-calculator size-12 rounded-xl shadow-xl",
                  resolvedTheme
                )}
                data-calculator-hidden-launcher=""
                data-theme={resolvedTheme}
              >
                <CalculatorIcon className="size-5" />
              </TooltipTrigger>
              <TooltipContent
                className={cn(
                  "mirai-calculator-package mirai-calculator-portal z-2147483002",
                  resolvedTheme
                )}
                data-theme={resolvedTheme}
              >
                Show calculator
              </TooltipContent>
            </Tooltip>
          )}
          <CalculatorViewportPortal fullscreen={fullscreen}>
            <div
              ref={calculatorRef}
              hidden={activeHidden}
              className={cn(
                "mirai-calculator @container relative flex h-full min-h-0 w-full min-w-0 max-w-full flex-col overflow-hidden rounded-xl border bg-background text-sm text-foreground tabular-nums shadow-xl [&_canvas]:block",
                resolvedTheme,
                fullscreen &&
                  "fixed inset-3 z-2147483000 h-[calc(100dvh-1.5rem)] min-h-0 w-auto max-w-none rounded-xl",
                className
              )}
              data-theme={resolvedTheme}
            >
              <header
                data-calculator-drag-handle=""
                className="mirai-calculator-header flex min-h-13 shrink-0 touch-none flex-nowrap items-center gap-3 overflow-hidden border-b bg-card px-3 py-2 select-none @max-[699px]:gap-1"
              >
                <span className="sr-only">{title}</span>
                <span
                  aria-hidden="true"
                  className="grid shrink-0 grid-cols-2 gap-0.75 p-0.5 @max-[359px]:hidden"
                >
                  {Array.from({ length: 6 }, (_, index) => (
                    <span key={index} className="size-0.75 rounded-full bg-muted-foreground/35" />
                  ))}
                </span>
                {enabledExtensions.length === 1 && (
                  <span
                    data-calculator-mode-label
                    className="flex h-8 min-w-0 shrink-0 items-center text-sm font-medium"
                  >
                    {CALCULATOR_EXTENSION_DEFINITIONS[enabledExtensions[0]].label}
                  </span>
                )}

                {enabledExtensions.length > 1 && (
                  <>
                    <div className="mirai-mode-select hidden min-w-0 flex-1 @max-[699px]:block">
                      <Select
                        value={activeMode}
                        onValueChange={(value: string | null) => {
                          if (
                            value !== null &&
                            enabledExtensions.includes(value as CalculatorExtension)
                          ) {
                            setMode(value as CalculatorExtension)
                          }
                        }}
                      >
                        <SelectTrigger
                          aria-label="Calculator mode"
                          size="sm"
                          className="h-8 w-full min-w-0 rounded-lg border-transparent bg-muted shadow-none"
                        >
                          <SelectValue>
                            {CALCULATOR_EXTENSION_DEFINITIONS[activeMode].label}
                          </SelectValue>
                        </SelectTrigger>
                        <SelectContent
                          className={cn(
                            "mirai-calculator-package mirai-calculator-portal z-2147483002",
                            resolvedTheme
                          )}
                          data-theme={resolvedTheme}
                        >
                          {enabledExtensions.map((extension) => (
                            <SelectItem key={extension} value={extension}>
                              {CALCULATOR_EXTENSION_DEFINITIONS[extension].label}
                            </SelectItem>
                          ))}
                        </SelectContent>
                      </Select>
                    </div>
                    <nav
                      aria-label="Calculator modes"
                      className="mirai-mode-switcher relative grid min-w-0 max-w-full flex-[1_1_364px] gap-0.5 overflow-hidden rounded-lg bg-muted p-0.75 @max-[699px]:hidden"
                      style={{
                        gridTemplateColumns: `repeat(${enabledExtensions.length}, minmax(0, 1fr))`,
                      }}
                    >
                      {enabledExtensions.map((extension) => (
                        <Button
                          key={extension}
                          aria-label={CALCULATOR_EXTENSION_DEFINITIONS[extension].label}
                          variant="ghost"
                          size="sm"
                          onClick={() => setMode(extension)}
                          aria-current={activeMode === extension ? "page" : undefined}
                          data-active={activeMode === extension ? "" : undefined}
                          className="h-7 min-w-0 w-full rounded-md bg-transparent px-1 text-sm font-medium shadow-none transition-[background-color,box-shadow,color] hover:bg-foreground/8! hover:text-foreground! focus-visible:border-transparent focus-visible:ring-0 focus-visible:outline-none data-active:bg-background data-active:text-foreground data-active:shadow-sm data-active:hover:bg-background!"
                        >
                          <span
                            aria-hidden="true"
                            className="mirai-mode-label @max-[699px]:text-xs"
                            data-compact-label={
                              CALCULATOR_EXTENSION_DEFINITIONS[extension].compactLabel
                            }
                          >
                            {CALCULATOR_EXTENSION_DEFINITIONS[extension].label}
                          </span>
                        </Button>
                      ))}
                    </nav>
                  </>
                )}

                <div className="mirai-header-actions ml-auto flex shrink-0 items-center gap-2 @max-[699px]:gap-1">
                  <CalculatorHeaderIconButton
                    label={`Use ${resolvedTheme === "dark" ? "light" : "dark"} mode`}
                    onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
                    portalTheme={resolvedTheme}
                  >
                    {resolvedTheme === "dark" ? <Sun /> : <Moon />}
                  </CalculatorHeaderIconButton>
                  <Button
                    type="button"
                    aria-label="Calculator settings"
                    variant="outline"
                    size="icon"
                    onClick={() => setSettingsOpen(true)}
                    className={cn(
                      "size-7.5 rounded-md",
                      settingsOpen && "bg-muted text-foreground"
                    )}
                  >
                    <Settings2 />
                  </Button>
                  <CalculatorSettingsDialog
                    settingsOpen={settingsOpen}
                    calculatorContainer={calculatorRef.current}
                    setSettingsOpen={setSettingsOpen}
                    resolvedTheme={resolvedTheme}
                    showsAngleMode={showsAngleMode}
                    activeAngleMode={activeAngleMode}
                    setAngleMode={setAngleMode}
                    displaySettings={displaySettings}
                    setDisplaySettings={setDisplaySettings}
                    showsGraphSettings={showsGraphSettings}
                    graphGridVisible={graphGridVisible}
                    setGraphGridVisible={setGraphGridVisible}
                    graphView={graphView}
                    updateGraphBoundary={updateGraphBoundary}
                    activeTheme={activeTheme}
                    setTheme={setTheme}
                  />
                  <CalculatorHeaderIconButton
                    label="Hide calculator"
                    onClick={() => setHidden(true)}
                    portalTheme={resolvedTheme}
                  >
                    <EyeOff />
                  </CalculatorHeaderIconButton>
                  <Button
                    aria-label="Reset"
                    variant="outline"
                    size="sm"
                    onClick={reset}
                    className="mirai-header-reset h-7.5 min-w-7.5 rounded-md px-2.5 text-xs text-muted-foreground @max-[699px]:w-7.5 @max-[699px]:px-0"
                  >
                    <RotateCcw
                      aria-hidden="true"
                      className="mirai-header-reset-icon hidden size-4 @max-[699px]:block"
                    />
                    <span className="mirai-header-reset-label @max-[699px]:hidden">Reset</span>
                  </Button>
                  <CalculatorHeaderIconButton
                    label={fullscreen ? "Exit full screen" : "Enter full screen"}
                    onClick={() => setFullscreen((value) => !value)}
                    portalTheme={resolvedTheme}
                  >
                    {fullscreen ? <Minimize2 /> : <Expand />}
                  </CalculatorHeaderIconButton>
                  {onClose && (
                    <CalculatorHeaderIconButton
                      label="Close calculator"
                      onClick={onClose}
                      portalTheme={resolvedTheme}
                    >
                      <X />
                    </CalculatorHeaderIconButton>
                  )}
                </div>
              </header>

              <main className="flex min-h-0 flex-1 flex-col overflow-hidden">
                {activeMode ? (
                  CALCULATOR_EXTENSION_DEFINITIONS[activeMode].render({
                    resetSignal,
                    engine,
                    activeAngleMode,
                    ans,
                    definitions,
                    setDefinitions,
                    setAns,
                    formatOptions,
                    formatNumber,
                    setVariables,
                    graphView,
                    setGraphView,
                    graphGridVisible,
                    setGraphGridVisible,
                    resolvedTheme,
                    defaultGraphingData,
                    defaultStatisticsData,
                    defaultToolsData,
                  })
                ) : (
                  <div
                    role="status"
                    className="flex min-h-48 flex-1 items-center justify-center p-8 text-center text-sm text-muted-foreground"
                  >
                    No calculator extensions enabled
                  </div>
                )}
              </main>
            </div>
          </CalculatorViewportPortal>
        </div>
      </div>
    </TooltipProvider>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/modes/graphing-canvas-viewport.tsx
import { Grid3X3, Home, LocateFixed, X, ZoomIn, ZoomOut } from "lucide-react"

import { CALCULATOR_CHART_TOKENS } from "@/components/mirai-calculator/calculator-ui-config"
import type {
  GraphMarkCluster,
  GraphTrace,
} from "@/components/mirai-calculator/modes/graphing-state"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { DEFAULT_GRAPH_VIEW } from "@openmirai/calculator-core/configuration"
import type { GraphView } from "@openmirai/calculator-core/graphing-view"
import { cn } from "@/lib/utils"

interface GraphTracePosition {
  x: number
  y: number
  labelOnLeft: boolean
  labelBelow: boolean
}

interface GraphingCanvasViewportProps {
  graphHostRef: React.RefObject<HTMLDivElement | null>
  canvasRef: React.RefObject<HTMLCanvasElement | null>
  trace: GraphTrace | null
  tracePosition: GraphTracePosition | null
  handlePointerDown: React.PointerEventHandler<HTMLCanvasElement>
  handlePointerMove: React.PointerEventHandler<HTMLCanvasElement>
  handlePointerUp: React.PointerEventHandler<HTMLCanvasElement>
  formatNumber: (value: number) => string
  zoom: (factor: number) => void
  setView: (view: GraphView) => void
  gridVisible: boolean
  onGridVisibleChange: (visible: boolean) => void
  analysisOpen: boolean
  setAnalysisOpen: (open: boolean) => void
  marks: Array<GraphMarkCluster>
  setTrace: (trace: GraphTrace | null) => void
}

/** Renders the interactive graph canvas, controls, trace label, and analysis overlay. */
export function GraphingCanvasViewport({
  graphHostRef,
  canvasRef,
  trace,
  tracePosition,
  handlePointerDown,
  handlePointerMove,
  handlePointerUp,
  formatNumber,
  zoom,
  setView,
  gridVisible,
  onGridVisibleChange,
  analysisOpen,
  setAnalysisOpen,
  marks,
  setTrace,
}: GraphingCanvasViewportProps) {
  return (
    <section className="mirai-graphing-canvas relative min-h-0 overflow-hidden overscroll-contain bg-background @max-[699px]:order-1 @max-[699px]:min-h-80">
      <div ref={graphHostRef} className="absolute inset-0" aria-label="Interactive graph">
        <canvas
          ref={canvasRef}
          className="size-full touch-none cursor-crosshair"
          onPointerDown={handlePointerDown}
          onPointerMove={handlePointerMove}
          onPointerUp={handlePointerUp}
          onPointerCancel={handlePointerUp}
        />
        {trace && tracePosition && (
          <div
            data-graph-trace-coordinate=""
            className="pointer-events-none absolute z-20"
            style={{ left: tracePosition.x, top: tracePosition.y }}
          >
            <span
              aria-hidden="true"
              className="absolute size-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-background shadow-sm"
              style={{ backgroundColor: trace.color }}
            />
            <output
              aria-live="polite"
              className={cn(
                "absolute w-max max-w-48 rounded-md border bg-background/95 px-2 py-1 font-mono text-xs text-foreground shadow-md backdrop-blur",
                tracePosition.labelOnLeft ? "right-3" : "left-3",
                tracePosition.labelBelow ? "top-3" : "bottom-3"
              )}
            >
              ({formatNumber(trace.x)}, {formatNumber(trace.y)})
            </output>
          </div>
        )}
      </div>

      <div className="absolute top-3 left-3 flex rounded-lg border bg-background/95 p-1 shadow-sm backdrop-blur">
        <Button variant="ghost" size="icon-sm" onClick={() => zoom(0.7)} aria-label="Zoom in">
          <ZoomIn />
        </Button>
        <Button variant="ghost" size="icon-sm" onClick={() => zoom(1 / 0.7)} aria-label="Zoom out">
          <ZoomOut />
        </Button>
        <Button
          variant="ghost"
          size="icon-sm"
          onClick={() => setView({ ...DEFAULT_GRAPH_VIEW })}
          aria-label="Reset graph view"
        >
          <Home />
        </Button>
        <Button
          variant={gridVisible ? "secondary" : "ghost"}
          size="icon-sm"
          onClick={() => onGridVisibleChange(!gridVisible)}
          aria-label="Toggle grid"
        >
          <Grid3X3 />
        </Button>
      </div>

      {analysisOpen ? (
        <div
          data-graph-analysis
          className="absolute right-3 bottom-3 max-h-[48%] w-[min(300px,calc(100%-1.5rem))] overflow-auto rounded-lg border bg-background/95 p-3 shadow-sm backdrop-blur"
        >
          <div className="mb-2 flex items-center justify-between gap-2">
            <h3 className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide">
              <LocateFixed className="size-3.5 text-primary" />
              Analysis
            </h3>
            <div className="flex items-center">
              {trace && (
                <Button variant="ghost" size="sm" onClick={() => setTrace(null)}>
                  Clear trace
                </Button>
              )}
              <Button
                variant="ghost"
                size="icon-sm"
                onClick={() => setAnalysisOpen(false)}
                aria-label="Hide analysis"
              >
                <X />
              </Button>
            </div>
          </div>
          {trace && (
            <>
              <div className="rounded-md bg-muted px-3 py-2 font-mono text-xs">
                ({formatNumber(trace.x)}, {formatNumber(trace.y)})
              </div>
              <Separator className="my-2" />
            </>
          )}
          <div className="space-y-1">
            {marks.slice(0, 12).map((mark, index) => (
              <Button
                variant="ghost"
                key={`${mark.labels.join("-")}-${mark.x}-${index}`}
                onClick={() =>
                  setTrace({
                    x: mark.x,
                    y: mark.y,
                    color: mark.colors.at(-1) ?? CALCULATOR_CHART_TOKENS[4],
                  })
                }
                className="h-auto w-full justify-between gap-2 px-2 py-1.5 text-xs"
              >
                <span className="truncate capitalize">
                  {mark.labels.join(" · ")}
                  {mark.count > 1 ? ` ×${String(mark.count)}` : ""}
                </span>
                <span className="shrink-0 font-mono text-muted-foreground">
                  ({formatNumber(mark.x)}, {formatNumber(mark.y)})
                </span>
              </Button>
            ))}
            {marks.length === 0 && !trace && (
              <p className="py-2 text-xs text-muted-foreground">
                Touch a curve to trace and drag it. Zeros, extrema, and intersections appear here.
              </p>
            )}
          </div>
        </div>
      ) : (
        <Button
          variant="outline"
          size="sm"
          onClick={() => setAnalysisOpen(true)}
          className="absolute right-3 bottom-3 bg-background/95 shadow-sm backdrop-blur"
        >
          <LocateFixed />
          Analysis
        </Button>
      )}
    </section>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/modes/graphing-sidebar.tsx
import { Play, Plus, Square, X } from "lucide-react"

import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import { CalculatorEmptyStateAction } from "@/components/mirai-calculator/calculator-empty-state-action"
import type {
  GraphExpressionRow,
  GraphTableColumn,
  GraphTableRow,
  TableGraphPoint,
} from "@/components/mirai-calculator/modes/graphing-state"
import type { CompiledGraphExpression } from "@openmirai/calculator-core/graphing"
import type { RegressionResult } from "@openmirai/calculator-core/statistics"
import { cn } from "@/lib/utils"

interface GraphingSidebarProps {
  compiled: Array<{ row: GraphExpressionRow; expression: CompiledGraphExpression }>
  rows: Array<GraphExpressionRow>
  tableColumns: Array<GraphTableColumn>
  table: Array<GraphTableRow>
  tableMinimumWidth: number
  tableSeries: Array<{
    column: GraphTableColumn & { color: string }
    points: Array<TableGraphPoint>
    regression: RegressionResult
  }>
  formatNumber: (value: number) => string
  updateRow: (id: number, patch: Partial<GraphExpressionRow>) => void
  commitRows: React.Dispatch<React.SetStateAction<Array<GraphExpressionRow>>>
  addTableVariable: () => void
  addTableRow: () => void
  setTable: React.Dispatch<React.SetStateAction<Array<GraphTableRow>>>
  setTableColumns: React.Dispatch<React.SetStateAction<Array<GraphTableColumn>>>
  removeTableVariable: (columnId: number) => void
  updateCell: (rowId: number, columnId: number, value: string) => void
  addExpression: () => void
}

function describeGraphExpression(
  expression: CompiledGraphExpression,
  formatNumber: (value: number) => string
): string {
  switch (expression.kind) {
    case "invalid":
      return expression.message
    case "explicit":
      return "Plotted"
    case "implicit":
      return "Implicit relation"
    case "points":
      return `${expression.points.length} plotted points`
    case "variable":
      return `${expression.name} = ${formatNumber(expression.value)}`
  }
}

/** Renders graph expressions, editable table data, and regression summaries. */
export function GraphingSidebar({
  compiled,
  rows,
  tableColumns,
  table,
  tableMinimumWidth,
  tableSeries,
  formatNumber,
  updateRow,
  commitRows,
  addTableVariable,
  addTableRow,
  setTable,
  setTableColumns,
  removeTableVariable,
  updateCell,
  addExpression,
}: GraphingSidebarProps) {
  return (
    <aside className="mirai-graphing-sidebar flex min-h-0 flex-col border-r bg-card @max-[699px]:order-2 @max-[699px]:min-h-75 @max-[699px]:max-h-[50%] @max-[699px]:border-r-0 @max-[699px]:border-b">
      <ScrollArea className="min-h-0 flex-1">
        <div className="divide-y">
          {compiled.map(({ row, expression }) => {
            const slider = row.slider
            return (
              <div key={row.id} className="flex gap-2.5 px-3 py-3">
                <Button
                  variant="ghost"
                  size="icon"
                  onClick={() => updateRow(row.id, { visible: !row.visible })}
                  aria-label={row.visible ? "Hide expression" : "Show expression"}
                  className="mt-0.5 size-5.5 shrink-0 rounded-full border-2 p-0"
                  style={{
                    borderColor: row.color,
                    backgroundColor: row.visible ? row.color : "transparent",
                  }}
                >
                  <span className="sr-only">{row.visible ? "Visible" : "Hidden"}</span>
                </Button>
                <div className="min-w-0 flex-1 space-y-2">
                  <div className="flex items-center gap-1.5">
                    <Input
                      value={row.text}
                      onChange={(event) => updateRow(row.id, { text: event.target.value })}
                      aria-label={`Graph expression ${row.id}`}
                      placeholder="y = 2x + 5"
                      className="h-7 min-w-0 rounded-none border-0 border-b border-transparent bg-transparent px-0 font-mono text-base font-medium shadow-none focus-visible:border-primary focus-visible:ring-0 dark:bg-transparent"
                    />
                    <Button
                      variant="ghost"
                      size="icon-sm"
                      onClick={() =>
                        commitRows((current) => current.filter((item) => item.id !== row.id))
                      }
                      aria-label={`Delete expression ${row.id}`}
                      className="text-muted-foreground hover:text-destructive"
                    >
                      <X />
                    </Button>
                  </div>
                  <p
                    className={cn(
                      "text-xs text-muted-foreground",
                      expression.kind === "invalid" && "text-destructive"
                    )}
                  >
                    {describeGraphExpression(expression, formatNumber)}
                  </p>
                  {slider && (
                    <div className="flex items-center gap-2">
                      <span className="text-xs text-muted-foreground">{slider.min}</span>
                      <Input
                        type="range"
                        min={slider.min}
                        max={slider.max}
                        step={slider.step}
                        value={slider.value}
                        onChange={(event) =>
                          updateRow(row.id, {
                            slider: {
                              ...slider,
                              value: Number(event.target.value),
                            },
                          })
                        }
                        aria-label={`Slider for expression ${row.id}`}
                        className="h-5 flex-1 border-0 p-0 shadow-none"
                      />
                      <span className="text-xs text-muted-foreground">{slider.max}</span>
                      <Button
                        variant="outline"
                        size="icon-sm"
                        onClick={() =>
                          updateRow(row.id, {
                            slider: {
                              ...slider,
                              playing: !slider.playing,
                            },
                          })
                        }
                        aria-label={slider.playing ? "Stop slider" : "Play slider"}
                        className="rounded-full text-primary"
                      >
                        {slider.playing ? <Square /> : <Play />}
                      </Button>
                    </div>
                  )}
                </div>
              </div>
            )
          })}

          {rows.length === 0 && (
            <div className="px-4 py-6 text-center">
              <p className="text-sm font-semibold">No expressions yet</p>
              <p className="mt-1 text-xs text-muted-foreground">
                <CalculatorEmptyStateAction label="Add expression" onClick={addExpression} /> one to
                start graphing.
              </p>
            </div>
          )}

          <section className="p-3">
            <div className="mb-3 flex flex-wrap items-center justify-end gap-2">
              <h3 className="mr-auto text-sm font-semibold">Table 1</h3>
              <Button
                variant="outline"
                size="sm"
                onClick={addTableVariable}
                className="h-7 rounded-md text-xs"
              >
                <Plus />
                Variable
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={addTableRow}
                className="h-7 rounded-md text-xs"
              >
                <Plus />
                Add row
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() => setTable([])}
                className="h-7 rounded-md text-xs"
              >
                Clear
              </Button>
            </div>
            <div className="mirai-graphing-table-scroll min-w-0 max-w-full overflow-x-auto overscroll-x-contain border-y contain-[inline-size]">
              <table
                className="w-full table-fixed border-collapse text-sm"
                style={{ minWidth: tableMinimumWidth }}
              >
                <colgroup>
                  <col className="w-9" />
                  {tableColumns.map((column) => (
                    <col className="w-28" key={column.id} />
                  ))}
                  <col className="w-8" />
                </colgroup>
                <thead className="bg-muted/35">
                  <tr>
                    <th
                      scope="col"
                      className="w-9 border-r px-2 py-1 text-center font-mono text-xs text-muted-foreground"
                    >
                      #
                    </th>
                    {tableColumns.map((column) => (
                      <th key={column.id} scope="col" className="border-r p-0 font-normal">
                        <div className="flex items-center">
                          {column.color && (
                            <span
                              aria-hidden="true"
                              className="ml-2 size-2.5 shrink-0 rounded-full"
                              style={{ backgroundColor: column.color }}
                            />
                          )}
                          <Input
                            value={column.label}
                            onChange={(event) =>
                              setTableColumns((current) =>
                                current.map((item) =>
                                  item.id === column.id
                                    ? { ...item, label: event.target.value }
                                    : item
                                )
                              )
                            }
                            aria-label={`Table variable ${column.label}`}
                            className="h-8 w-full min-w-0 rounded-none border-0 bg-transparent px-2 font-mono text-sm font-semibold shadow-none focus-visible:ring-0 dark:bg-transparent"
                          />
                          {column.role === "y" && (
                            <Button
                              variant="ghost"
                              size="icon-sm"
                              onClick={() => removeTableVariable(column.id)}
                              aria-label={`Remove variable ${column.label}`}
                              className="rounded-none text-muted-foreground hover:text-destructive"
                            >
                              <X />
                            </Button>
                          )}
                        </div>
                      </th>
                    ))}
                    <th scope="col" className="w-8">
                      <span className="sr-only">Row actions</span>
                    </th>
                  </tr>
                </thead>
                <tbody>
                  {table.map((row, rowIndex) => (
                    <tr key={row.id} className="border-t">
                      <th
                        scope="row"
                        className="border-r px-2 text-center font-mono text-xs font-normal text-muted-foreground"
                      >
                        {rowIndex + 1}
                      </th>
                      {tableColumns.map((column) => (
                        <td key={column.id} className="border-r p-0">
                          <Input
                            value={row.values[column.id] ?? ""}
                            onChange={(event) => updateCell(row.id, column.id, event.target.value)}
                            aria-label={`${column.label} row ${rowIndex + 1}`}
                            className="h-8 w-full min-w-0 rounded-none border-0 bg-transparent px-2 font-mono text-sm shadow-none focus-visible:ring-1 dark:bg-transparent"
                          />
                        </td>
                      ))}
                      <td className="p-0">
                        <Button
                          variant="ghost"
                          size="icon-sm"
                          onClick={() =>
                            setTable((current) => current.filter((item) => item.id !== row.id))
                          }
                          aria-label={`Remove table row ${rowIndex + 1}`}
                          className="rounded-none text-muted-foreground hover:text-destructive"
                        >
                          <X />
                        </Button>
                      </td>
                    </tr>
                  ))}
                  {table.length === 0 && (
                    <tr>
                      <td
                        colSpan={tableColumns.length + 2}
                        className="h-12 px-3 text-center text-xs text-muted-foreground"
                      >
                        <CalculatorEmptyStateAction label="Add table row" onClick={addTableRow} /> a
                        row to plot table data.
                      </td>
                    </tr>
                  )}
                </tbody>
              </table>
            </div>
          </section>

          <section className="p-3">
            <h3 className="mb-3 text-sm font-semibold">Regression</h3>
            <div className="space-y-3">
              {tableSeries.map(({ column, regression }) => (
                <div key={column.id}>
                  <Badge variant="secondary" className="mb-2 font-mono text-xs">
                    {column.label} ~ m{tableColumns[0]?.label ?? "x"} + b
                  </Badge>
                  <div className="grid grid-cols-3 gap-2">
                    {[
                      ["m", regression.params[1]?.value],
                      ["b", regression.params[0]?.value],
                      ["R²", regression.r2],
                    ].map(([label, value]) => (
                      <div key={String(label)} className="rounded-lg border p-2">
                        <div className="text-xs text-muted-foreground">{label}</div>
                        <div
                          className={cn(
                            "font-mono text-sm font-semibold",
                            label === "R²" && "text-primary"
                          )}
                        >
                          {typeof value === "number" && Number.isFinite(value)
                            ? formatNumber(value)
                            : "—"}
                        </div>
                      </div>
                    ))}
                  </div>
                </div>
              ))}
              {tableSeries.length === 0 && (
                <p className="text-xs text-muted-foreground">
                  <CalculatorEmptyStateAction label="Add y variable" onClick={addTableVariable} /> a
                  y variable to fit data.
                </p>
              )}
            </div>
          </section>
        </div>
      </ScrollArea>

      <div className="flex shrink-0 gap-2 border-t bg-muted/35 p-3">
        <Button variant="outline" size="sm" onClick={addExpression} className="flex-1 rounded-md">
          <Plus />
          Expression
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => commitRows([])}
          className="rounded-md text-muted-foreground hover:text-destructive"
        >
          Clear all
        </Button>
      </div>
    </aside>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/modes/graphing-state.ts
import { CALCULATOR_CHART_TOKENS } from "@/components/mirai-calculator/calculator-ui-config"
import type { GraphingInitialData } from "@openmirai/calculator-core/graphing-data"
import type { GraphPoint } from "@openmirai/calculator-core/graphing-view"

/** Editable graph expression state owned by the graphing interface. */
export interface GraphExpressionRow {
  id: number
  text: string
  color: string
  visible: boolean
  slider?: {
    min: number
    max: number
    step: number
    value: number
    playing: boolean
  }
}

/** Editable table row keyed by graph table column identifiers. */
export interface GraphTableRow {
  id: number
  values: Record<number, string>
}

/** Editable graph table column. */
export interface GraphTableColumn {
  id: number
  label: string
  color?: string
  role: "x" | "y"
}

/** Clustered graph-analysis point with presentation metadata. */
export interface GraphMarkCluster extends GraphPoint {
  labels: Array<string>
  colors: Array<string>
  count: number
}

/** Plottable table point connected to its editable source cells. */
export interface TableGraphPoint extends GraphPoint {
  rowId: number
  columnId: number
  color: string
}

/** Active trace point and its optional editable source. */
export interface GraphTrace extends GraphPoint {
  color: string
  source?:
    | { kind: "expression"; rowId: number }
    | { kind: "table"; rowId: number; columnId: number }
}

/** Maximum pointer distance in viewport pixels for selecting a graph point. */
export const GRAPH_POINT_HIT_RADIUS = 14

/** Maximum number of implicit-render cache entries retained by the graph canvas. */
export const IMPLICIT_CACHE_LIMIT = 24

/** Fixed graph-table index column width in pixels for canvas-independent table sizing. */
export const TABLE_INDEX_COLUMN_WIDTH = 36

/** Fixed editable graph-table value column width. */
export const TABLE_VALUE_COLUMN_WIDTH = 112

/** Fixed graph-table row-action column width. */
export const TABLE_ACTION_COLUMN_WIDTH = 32

/** Converts consumer graphing defaults into editable expression rows. */
export function createInitialGraphExpressionRows(
  data: GraphingInitialData | undefined
): Array<GraphExpressionRow> {
  return (data?.expressions ?? []).map((expression, index) => ({
    id: index + 1,
    text: expression.value,
    color: expression.color ?? CALCULATOR_CHART_TOKENS[index % CALCULATOR_CHART_TOKENS.length],
    visible: expression.visible ?? true,
    slider: expression.slider
      ? { ...expression.slider, playing: expression.slider.playing ?? false }
      : undefined,
  }))
}

/** Converts consumer graphing defaults into editable multi-variable table state. */
export function createInitialGraphTableState(data: GraphingInitialData | undefined): {
  columns: Array<GraphTableColumn>
  rows: Array<GraphTableRow>
} {
  const table = data?.table
  const series = table?.series?.length
    ? table.series
    : [{ label: "y₁", color: CALCULATOR_CHART_TOKENS[2], values: [] }]
  const columns: Array<GraphTableColumn> = [
    { id: 1, label: table?.xLabel ?? "x₁", role: "x" },
    ...series.map((item, index) => ({
      id: index + 2,
      label: item.label ?? `y${String(index + 1)}`,
      color: item.color ?? CALCULATOR_CHART_TOKENS[(index + 2) % CALCULATOR_CHART_TOKENS.length],
      role: "y" as const,
    })),
  ]
  const rowCount = Math.max(
    table?.xValues?.length ?? 0,
    ...series.map((item) => item.values.length)
  )
  const rows = Array.from({ length: rowCount }, (_, rowIndex) => ({
    id: rowIndex + 1,
    values: Object.fromEntries([
      [1, String(table?.xValues?.[rowIndex] ?? "")],
      ...series.map((item, seriesIndex) => [seriesIndex + 2, String(item.values[rowIndex] ?? "")]),
    ]),
  }))
  return { columns, rows }
}
../../packages/calculator-registry/src/components/mirai-calculator/modes/graphing-mode.tsx
import { useEffect, useCallback, useMemo, useRef, useState } from "react"
import { readCanvasColor, resolveCanvasColor } from "@/components/mirai-calculator/canvas-theme"
import { CALCULATOR_CHART_TOKENS } from "@/components/mirai-calculator/calculator-ui-config"
import { GraphingCanvasViewport } from "@/components/mirai-calculator/modes/graphing-canvas-viewport"
import { GraphingSidebar } from "@/components/mirai-calculator/modes/graphing-sidebar"
import {
  GRAPH_POINT_HIT_RADIUS,
  IMPLICIT_CACHE_LIMIT,
  TABLE_ACTION_COLUMN_WIDTH,
  TABLE_INDEX_COLUMN_WIDTH,
  TABLE_VALUE_COLUMN_WIDTH,
  createInitialGraphExpressionRows,
  createInitialGraphTableState,
} from "@/components/mirai-calculator/modes/graphing-state"
import type {
  GraphExpressionRow,
  GraphMarkCluster,
  GraphTableColumn,
  GraphTableRow,
  GraphTrace,
  TableGraphPoint,
} from "@/components/mirai-calculator/modes/graphing-state"
import { collectSliderVariables } from "@openmirai/calculator-core/configuration"
import { CalculatorEngine } from "@openmirai/calculator-core/engine"
import type { AngleMode } from "@openmirai/calculator-core/engine"
import {
  clusterGraphPoints,
  compileGraphExpression,
  findExtrema,
  findIntersections,
  findRoots,
  graphGridStep,
  sampleExplicitGraphSegments,
  sampleImplicitContourSegments,
} from "@openmirai/calculator-core/graphing"
import type { CompiledGraphExpression } from "@openmirai/calculator-core/graphing"
import type { GraphingInitialData } from "@openmirai/calculator-core/graphing-data"
import {
  fitGraphViewToAspect,
  graphPointToViewport,
  panGraphView,
  projectPointToGraphSegments,
  viewportPointToGraph,
  zoomGraphView,
} from "@openmirai/calculator-core/graphing-view"
import type { GraphPoint, GraphSegment, GraphView } from "@openmirai/calculator-core/graphing-view"
import { fitRegression } from "@openmirai/calculator-core/statistics"

type ExplicitYExpression = Extract<CompiledGraphExpression, { kind: "explicit" }> & { axis: "y" }

export interface GraphingModeProps {
  angleMode: AngleMode
  ans: number
  definitions: Array<string>
  formatNumber: (value: number) => string
  onVariablesChange: (variables: Record<string, number>) => void
  view: GraphView
  onViewChange: (view: GraphView) => void
  gridVisible: boolean
  onGridVisibleChange: (visible: boolean) => void
  colorScheme?: "light" | "dark"
  defaultData?: GraphingInitialData
}

/** Renders the interactive graph workspace with expressions, tables, tracing, and analysis. */
export function GraphingMode({
  angleMode,
  ans,
  definitions,
  formatNumber: formatValue,
  onVariablesChange,
  view,
  onViewChange,
  gridVisible,
  onGridVisibleChange,
  colorScheme = "light",
  defaultData,
}: GraphingModeProps) {
  const [initialTable] = useState(() => createInitialGraphTableState(defaultData))
  const [rows, setRows] = useState<Array<GraphExpressionRow>>(() =>
    createInitialGraphExpressionRows(defaultData)
  )
  const rowsRef = useRef(rows)
  const [tableColumns, setTableColumns] = useState<Array<GraphTableColumn>>(initialTable.columns)
  const [table, setTable] = useState<Array<GraphTableRow>>(initialTable.rows)
  const tableMinimumWidth =
    TABLE_INDEX_COLUMN_WIDTH +
    tableColumns.length * TABLE_VALUE_COLUMN_WIDTH +
    TABLE_ACTION_COLUMN_WIDTH
  const [trace, setTrace] = useState<GraphTrace | null>(null)
  const [analysisOpen, setAnalysisOpen] = useState(false)
  const [interacting, setInteracting] = useState(false)
  const [graphSize, setGraphSize] = useState({ width: 0, height: 0 })
  const nextRowId = useRef(rows.length + 1)
  const nextTableRowId = useRef(table.length + 1)
  const nextTableColumnId = useRef(tableColumns.length + 1)
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const graphHostRef = useRef<HTMLDivElement>(null)
  const [renderedSegmentsByRow] = useState(() => new Map<number, Array<GraphSegment>>())
  const [implicitCache] = useState(
    () =>
      new Map<
        number,
        {
          residual: (x: number, y: number) => number
          key: string
          segments: ReturnType<typeof sampleImplicitContourSegments>
        }
      >()
  )
  const interactionTimerRef = useRef<number | null>(null)
  const dragRef = useRef<
    | {
        kind: "view"
        pointerId: number
        startX: number
        startY: number
        view: GraphView
        renderedView: GraphView
        moved: boolean
      }
    | { kind: "trace"; pointerId: number; rowId: number; color: string }
    | { kind: "table"; pointerId: number; rowId: number; columnId: number; color: string }
    | null
  >(null)
  const formatNumber = useCallback(
    (value: number) => formatValue(Number.isFinite(value) ? Number(value.toPrecision(7)) : value),
    [formatValue]
  )
  const commitRows = useCallback(
    (
      update:
        | Array<GraphExpressionRow>
        | ((current: Array<GraphExpressionRow>) => Array<GraphExpressionRow>)
    ) => {
      const nextRows = typeof update === "function" ? update(rowsRef.current) : update
      rowsRef.current = nextRows
      setRows(nextRows)
      onVariablesChange(
        collectSliderVariables(
          nextRows.map((row) => ({ expression: row.text, value: row.slider?.value }))
        )
      )
    },
    [onVariablesChange]
  )
  const tracePosition = useMemo(() => {
    if (!trace || graphSize.width <= 0 || graphSize.height <= 0) {
      return null
    }
    const renderedView = fitGraphViewToAspect(view, graphSize.width, graphSize.height)
    const point = graphPointToViewport(trace, renderedView, graphSize.width, graphSize.height)
    if (!point) {
      return null
    }
    return {
      ...point,
      labelOnLeft: trace.x > (renderedView.xmin + renderedView.xmax) / 2,
      labelBelow: trace.y > renderedView.ymax - (renderedView.ymax - renderedView.ymin) * 0.12,
    }
  }, [graphSize.height, graphSize.width, trace, view])

  const setView = useCallback(
    (next: GraphView | ((current: GraphView) => GraphView)) => {
      onViewChange(typeof next === "function" ? next(view) : next)
    },
    [onViewChange, view]
  )

  const variables = useMemo(
    () =>
      collectSliderVariables(
        rows.map((row) => ({ expression: row.text, value: row.slider?.value }))
      ),
    [rows]
  )

  const engine = useMemo(
    () =>
      new CalculatorEngine({
        angleMode,
        ans,
        definitions,
        variables,
      }),
    [angleMode, ans, definitions, variables]
  )

  const compiled = useMemo(
    () =>
      rows.map((row) => ({
        row,
        expression: compileGraphExpression(row.text, engine),
      })),
    [engine, rows]
  )

  const tableSeries = useMemo(() => {
    const xColumn = tableColumns.find((column) => column.role === "x")
    if (!xColumn) {
      return []
    }
    return tableColumns
      .filter(
        (column): column is GraphTableColumn & { color: string } =>
          column.role === "y" && !!column.color
      )
      .map((column) => {
        const points = table.flatMap<TableGraphPoint>((row) => {
          const x = Number(row.values[xColumn.id])
          const y = Number(row.values[column.id])
          return Number.isFinite(x) && Number.isFinite(y)
            ? [{ x, y, rowId: row.id, columnId: column.id, color: column.color }]
            : []
        })
        return {
          column,
          points,
          regression: fitRegression(
            points.map((point) => point.x),
            points.map((point) => point.y),
            "linear"
          ),
        }
      })
  }, [table, tableColumns])
  const tablePoints = useMemo(() => tableSeries.flatMap((series) => series.points), [tableSeries])

  const marks = useMemo<Array<GraphMarkCluster>>(() => {
    if (!analysisOpen) {
      return []
    }
    const visibleFunctions = compiled.filter(
      (
        item
      ): item is {
        row: GraphExpressionRow
        expression: ExplicitYExpression
      } => item.row.visible && item.expression.kind === "explicit" && item.expression.axis === "y"
    )
    const result: Array<GraphPoint & { label: string; color: string }> = []

    for (const item of visibleFunctions) {
      try {
        for (const x of findRoots(item.expression.evaluate, view.xmin, view.xmax)) {
          result.push({ x, y: 0, label: "zero", color: item.row.color })
        }
        for (const point of findExtrema(item.expression.evaluate, view.xmin, view.xmax)) {
          result.push({
            x: point.x,
            y: point.y,
            label: point.kind,
            color: item.row.color,
          })
        }
      } catch {
        // A partially defined function is still allowed to render where possible.
      }
    }

    for (let left = 0; left < visibleFunctions.length; left += 1) {
      for (let right = left + 1; right < visibleFunctions.length; right += 1) {
        try {
          for (const point of findIntersections(
            visibleFunctions[left].expression.evaluate,
            visibleFunctions[right].expression.evaluate,
            view.xmin,
            view.xmax
          )) {
            result.push({
              ...point,
              label: "intersection",
              color: CALCULATOR_CHART_TOKENS[4],
            })
          }
        } catch {
          // Ignore an invalid pair while keeping the remaining graph usable.
        }
      }
    }
    const tolerance = Math.max(view.xmax - view.xmin, view.ymax - view.ymin) * 1e-6
    return clusterGraphPoints(result.slice(0, 50), tolerance).map(({ point, indexes }) => ({
      ...point,
      labels: [...new Set(indexes.map((index) => result[index].label))],
      colors: [...new Set(indexes.map((index) => result[index].color))],
      count: indexes.length,
    }))
  }, [analysisOpen, compiled, view.xmax, view.xmin, view.ymax, view.ymin])

  useEffect(() => {
    const canvas = canvasRef.current
    const host = graphHostRef.current
    if (!canvas || !host) {
      return
    }

    const draw = () => {
      const bounds = host.getBoundingClientRect()
      const width = Math.max(1, Math.floor(bounds.width))
      const height = Math.max(1, Math.floor(bounds.height))
      setGraphSize((current) =>
        current.width === width && current.height === height ? current : { width, height }
      )
      const ratio = Math.min(window.devicePixelRatio || 1, 2)
      canvas.width = width * ratio
      canvas.height = height * ratio
      canvas.style.width = `${width}px`
      canvas.style.height = `${height}px`
      const context = canvas.getContext("2d")
      if (!context) {
        return
      }
      context.setTransform(ratio, 0, 0, ratio, 0, 0)

      const background = readCanvasColor(canvas, "--background")
      const border = readCanvasColor(canvas, "--border")
      const muted = readCanvasColor(canvas, "--muted-foreground")
      const renderedView = fitGraphViewToAspect(view, width, height)
      const xToCanvas = (x: number) =>
        ((x - renderedView.xmin) / (renderedView.xmax - renderedView.xmin)) * width
      const yToCanvas = (y: number) =>
        height - ((y - renderedView.ymin) / (renderedView.ymax - renderedView.ymin)) * height
      const renderedSegments = new Map<number, Array<GraphSegment>>()

      context.clearRect(0, 0, width, height)
      context.fillStyle = background
      context.fillRect(0, 0, width, height)

      if (gridVisible) {
        const xStep = graphGridStep(renderedView.xmax - renderedView.xmin)
        const yStep = graphGridStep(renderedView.ymax - renderedView.ymin)
        context.strokeStyle = border
        context.lineWidth = 1
        context.font = "10px system-ui, -apple-system, sans-serif"
        context.fillStyle = muted

        for (
          let x = Math.ceil(renderedView.xmin / xStep) * xStep;
          x <= renderedView.xmax;
          x += xStep
        ) {
          const pixel = xToCanvas(x)
          context.beginPath()
          context.moveTo(pixel, 0)
          context.lineTo(pixel, height)
          context.stroke()
          if (Math.abs(x) > 1e-10) {
            context.fillText(formatNumber(x), pixel + 4, yToCanvas(0) + 12)
          }
        }
        for (
          let y = Math.ceil(renderedView.ymin / yStep) * yStep;
          y <= renderedView.ymax;
          y += yStep
        ) {
          const pixel = yToCanvas(y)
          context.beginPath()
          context.moveTo(0, pixel)
          context.lineTo(width, pixel)
          context.stroke()
          if (Math.abs(y) > 1e-10) {
            context.fillText(formatNumber(y), xToCanvas(0) + 5, pixel - 4)
          }
        }
      }

      context.strokeStyle = muted
      context.lineWidth = 1.3
      if (renderedView.ymin <= 0 && renderedView.ymax >= 0) {
        context.beginPath()
        context.moveTo(0, yToCanvas(0))
        context.lineTo(width, yToCanvas(0))
        context.stroke()
      }
      if (renderedView.xmin <= 0 && renderedView.xmax >= 0) {
        context.beginPath()
        context.moveTo(xToCanvas(0), 0)
        context.lineTo(xToCanvas(0), height)
        context.stroke()
      }

      for (const { row, expression } of compiled) {
        if (!row.visible || expression.kind === "invalid") {
          continue
        }
        const rowColor = resolveCanvasColor(canvas, row.color)
        context.strokeStyle = rowColor
        context.fillStyle = rowColor
        context.lineWidth = 2.2
        context.lineCap = "round"
        context.lineJoin = "round"

        if (expression.kind === "explicit") {
          const segments = sampleExplicitGraphSegments(
            expression.evaluate,
            expression.axis,
            renderedView,
            width,
            height
          )
          context.beginPath()
          for (const segment of segments) {
            const { from, to } = segment
            context.moveTo(xToCanvas(from.x), yToCanvas(from.y))
            context.lineTo(xToCanvas(to.x), yToCanvas(to.y))
          }
          context.stroke()
          renderedSegments.set(row.id, segments)
        } else if (expression.kind === "points") {
          for (const point of expression.points) {
            context.beginPath()
            context.arc(xToCanvas(point.x), yToCanvas(point.y), 4.5, 0, Math.PI * 2)
            context.fill()
          }
        } else if (expression.kind === "implicit") {
          const columns = interacting
            ? Math.max(36, Math.min(72, Math.ceil(width / 14)))
            : Math.max(48, Math.min(120, Math.ceil(width / 9)))
          const rowsCount = interacting
            ? Math.max(32, Math.min(56, Math.ceil(height / 14)))
            : Math.max(40, Math.min(96, Math.ceil(height / 9)))
          const cacheKey = [
            renderedView.xmin,
            renderedView.xmax,
            renderedView.ymin,
            renderedView.ymax,
            columns,
            rowsCount,
          ].join(":")
          const cached = implicitCache.get(row.id)
          const segments =
            cached?.residual === expression.residual && cached.key === cacheKey
              ? cached.segments
              : sampleImplicitContourSegments(expression.residual, renderedView, columns, rowsCount)
          if (segments !== cached?.segments) {
            implicitCache.set(row.id, {
              residual: expression.residual,
              key: cacheKey,
              segments,
            })
            if (implicitCache.size > IMPLICIT_CACHE_LIMIT) {
              const oldestRowId = implicitCache.keys().next().value
              if (typeof oldestRowId === "number") {
                implicitCache.delete(oldestRowId)
              }
            }
          }
          const isolatedPoints: Array<GraphPoint> = []
          context.beginPath()
          for (const segment of segments) {
            const fromX = xToCanvas(segment.from.x)
            const fromY = yToCanvas(segment.from.y)
            const toX = xToCanvas(segment.to.x)
            const toY = yToCanvas(segment.to.y)
            if (segment.from.x === segment.to.x && segment.from.y === segment.to.y) {
              isolatedPoints.push({ x: fromX, y: fromY })
              continue
            }
            context.moveTo(fromX, fromY)
            context.lineTo(toX, toY)
          }
          context.stroke()
          for (const point of isolatedPoints) {
            context.beginPath()
            context.arc(point.x, point.y, 2.2, 0, Math.PI * 2)
            context.fill()
          }
          renderedSegments.set(row.id, segments)
        }
      }

      renderedSegmentsByRow.clear()
      for (const [rowId, segments] of renderedSegments) {
        renderedSegmentsByRow.set(rowId, segments)
      }

      for (const point of tablePoints) {
        context.fillStyle = resolveCanvasColor(canvas, point.color)
        context.beginPath()
        context.arc(xToCanvas(point.x), yToCanvas(point.y), 4.5, 0, Math.PI * 2)
        context.fill()
      }

      for (const mark of marks) {
        context.fillStyle = background
        context.strokeStyle = resolveCanvasColor(
          canvas,
          mark.colors.at(-1) ?? CALCULATOR_CHART_TOKENS[4]
        )
        context.lineWidth = 2
        context.beginPath()
        context.arc(xToCanvas(mark.x), yToCanvas(mark.y), 5, 0, Math.PI * 2)
        context.fill()
        context.stroke()
      }
    }

    let frame = window.requestAnimationFrame(draw)
    const scheduleDraw = () => {
      window.cancelAnimationFrame(frame)
      frame = window.requestAnimationFrame(draw)
    }
    const observer = new ResizeObserver(scheduleDraw)
    observer.observe(host)
    return () => {
      window.cancelAnimationFrame(frame)
      observer.disconnect()
    }
  }, [
    colorScheme,
    compiled,
    formatNumber,
    gridVisible,
    implicitCache,
    interacting,
    marks,
    renderedSegmentsByRow,
    tablePoints,
    view,
  ])

  const hasPlayingSliders = rows.some((row) => row.slider?.playing)

  useEffect(() => {
    if (!hasPlayingSliders) {
      return
    }
    const timer = window.setInterval(() => {
      commitRows((currentRows) =>
        currentRows.map((current) => {
          if (!current.slider?.playing) {
            return current
          }
          const next = current.slider.value + current.slider.step
          return {
            ...current,
            slider: {
              ...current.slider,
              value: next > current.slider.max ? current.slider.min : next,
            },
          }
        })
      )
    }, 100)
    return () => window.clearInterval(timer)
  }, [commitRows, hasPlayingSliders])

  const zoom = useCallback(
    (factor: number) => {
      setView((current) => zoomGraphView(current, factor))
    },
    [setView]
  )

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) {
      return
    }

    const handleWheel = (event: WheelEvent) => {
      event.preventDefault()
      event.stopPropagation()
      setInteracting(true)
      if (interactionTimerRef.current !== null) {
        window.clearTimeout(interactionTimerRef.current)
      }
      interactionTimerRef.current = window.setTimeout(() => {
        setInteracting(false)
        interactionTimerRef.current = null
      }, 140)
      zoom(event.deltaY > 0 ? 1.12 : 1 / 1.12)
    }

    canvas.addEventListener("wheel", handleWheel, { passive: false })
    return () => {
      canvas.removeEventListener("wheel", handleWheel)
      if (interactionTimerRef.current !== null) {
        window.clearTimeout(interactionTimerRef.current)
      }
    }
  }, [zoom])

  const graphCoordinates = (clientX: number, clientY: number): GraphPoint => {
    const canvas = canvasRef.current
    if (!canvas) {
      return { x: view.xmin, y: view.ymax }
    }
    const bounds = canvas.getBoundingClientRect()
    const renderedView = fitGraphViewToAspect(view, bounds.width, bounds.height)
    return (
      viewportPointToGraph(
        { x: clientX - bounds.left, y: clientY - bounds.top },
        renderedView,
        bounds.width,
        bounds.height
      ) ?? { x: renderedView.xmin, y: renderedView.ymax }
    )
  }

  const handlePointerDown = (event: React.PointerEvent<HTMLCanvasElement>) => {
    event.currentTarget.setPointerCapture(event.pointerId)
    const bounds = event.currentTarget.getBoundingClientRect()
    const measuredSize = {
      width: Math.max(1, Math.floor(bounds.width)),
      height: Math.max(1, Math.floor(bounds.height)),
    }
    setGraphSize((current) =>
      current.width === measuredSize.width && current.height === measuredSize.height
        ? current
        : measuredSize
    )
    const renderedView = fitGraphViewToAspect(view, bounds.width, bounds.height)
    const graphPoint = graphCoordinates(event.clientX, event.clientY)
    const pointerX = event.clientX - bounds.left
    const pointerY = event.clientY - bounds.top
    const tablePoint = [...tablePoints].reverse().find((point) => {
      const viewportPoint = graphPointToViewport(point, renderedView, bounds.width, bounds.height)
      return (
        viewportPoint !== null &&
        Math.hypot(viewportPoint.x - pointerX, viewportPoint.y - pointerY) <= GRAPH_POINT_HIT_RADIUS
      )
    })
    if (tablePoint) {
      setTrace({
        ...tablePoint,
        source: { kind: "table", rowId: tablePoint.rowId, columnId: tablePoint.columnId },
      })
      dragRef.current = {
        kind: "table",
        pointerId: event.pointerId,
        rowId: tablePoint.rowId,
        columnId: tablePoint.columnId,
        color: tablePoint.color,
      }
      setInteracting(true)
      return
    }

    let closestCurve: { row: GraphExpressionRow; point: GraphPoint } | null = null
    let closestDistance = GRAPH_POINT_HIT_RADIUS + 1
    for (const item of compiled) {
      const { expression, row } = item
      if (!row.visible || (expression.kind !== "explicit" && expression.kind !== "implicit")) {
        continue
      }
      const renderedSegments = renderedSegmentsByRow.get(row.id)
      const fallbackSegments =
        expression.kind === "explicit"
          ? sampleExplicitGraphSegments(
              expression.evaluate,
              expression.axis,
              renderedView,
              bounds.width,
              bounds.height
            )
          : []
      if (!renderedSegments && fallbackSegments.length > 0) {
        renderedSegmentsByRow.set(row.id, fallbackSegments)
      }
      const projection = projectPointToGraphSegments(
        renderedSegments ?? fallbackSegments,
        graphPoint,
        renderedView,
        bounds.width,
        bounds.height
      )
      if (projection && projection.distance <= closestDistance) {
        closestCurve = { row, point: projection.point }
        closestDistance = projection.distance
      }
    }
    if (closestCurve) {
      setTrace({
        ...closestCurve.point,
        color: closestCurve.row.color,
        source: { kind: "expression", rowId: closestCurve.row.id },
      })
      dragRef.current = {
        kind: "trace",
        pointerId: event.pointerId,
        rowId: closestCurve.row.id,
        color: closestCurve.row.color,
      }
      setInteracting(true)
      return
    }

    dragRef.current = {
      kind: "view",
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      view,
      renderedView,
      moved: false,
    }
    setInteracting(true)
  }

  const handlePointerMove = (event: React.PointerEvent<HTMLCanvasElement>) => {
    const drag = dragRef.current
    const bounds = event.currentTarget.getBoundingClientRect()
    if (!drag || drag.pointerId !== event.pointerId) {
      return
    }
    event.preventDefault()
    const point = graphCoordinates(event.clientX, event.clientY)

    if (drag.kind === "trace") {
      const renderedView = fitGraphViewToAspect(view, bounds.width, bounds.height)
      const projection = projectPointToGraphSegments(
        renderedSegmentsByRow.get(drag.rowId) ?? [],
        point,
        renderedView,
        bounds.width,
        bounds.height
      )
      if (projection) {
        setTrace({
          ...projection.point,
          color: drag.color,
          source: { kind: "expression", rowId: drag.rowId },
        })
      }
      return
    }

    if (drag.kind === "table") {
      const xColumn = tableColumns.find((column) => column.role === "x")
      if (!xColumn) {
        return
      }
      const xValue = String(Number(point.x.toPrecision(10)))
      const yValue = String(Number(point.y.toPrecision(10)))
      setTable((current) =>
        current.map((row) =>
          row.id === drag.rowId
            ? {
                ...row,
                values: {
                  ...row.values,
                  [xColumn.id]: xValue,
                  [drag.columnId]: yValue,
                },
              }
            : row
        )
      )
      setTrace({
        x: point.x,
        y: point.y,
        color: drag.color,
        source: { kind: "table", rowId: drag.rowId, columnId: drag.columnId },
      })
      return
    }

    const dx = event.clientX - drag.startX
    const dy = event.clientY - drag.startY
    if (Math.abs(dx) + Math.abs(dy) > 3) {
      drag.moved = true
    }
    setView(panGraphView(drag.view, drag.renderedView, dx, dy, bounds.width, bounds.height))
  }

  const handlePointerUp = (event: React.PointerEvent<HTMLCanvasElement>) => {
    const completedDrag = dragRef.current
    dragRef.current = null
    if (completedDrag?.kind === "trace" || completedDrag?.kind === "table") {
      setTrace(null)
    }
    setInteracting(false)
    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId)
    }
  }

  const updateRow = (id: number, patch: Partial<GraphExpressionRow>) => {
    commitRows((current) => current.map((row) => (row.id === id ? { ...row, ...patch } : row)))
  }

  const addExpression = () => {
    const id = nextRowId.current
    nextRowId.current += 1
    commitRows((current) => [
      ...current,
      {
        id,
        text: "",
        color: CALCULATOR_CHART_TOKENS[(id - 1) % CALCULATOR_CHART_TOKENS.length],
        visible: true,
      },
    ])
  }

  const updateCell = (rowId: number, columnId: number, value: string) => {
    setTable((current) =>
      current.map((row) =>
        row.id === rowId ? { ...row, values: { ...row.values, [columnId]: value } } : row
      )
    )
  }

  const addTableRow = () => {
    const id = nextTableRowId.current
    nextTableRowId.current += 1
    setTable((current) => [
      ...current,
      { id, values: Object.fromEntries(tableColumns.map((column) => [column.id, ""])) },
    ])
  }

  const addTableVariable = () => {
    const id = nextTableColumnId.current
    nextTableColumnId.current += 1
    const variableIndex = tableColumns.filter((column) => column.role === "y").length + 1
    setTableColumns((current) => [
      ...current,
      {
        id,
        label: `y${String(variableIndex)}`,
        color: CALCULATOR_CHART_TOKENS[(variableIndex + 1) % CALCULATOR_CHART_TOKENS.length],
        role: "y",
      },
    ])
  }

  const removeTableVariable = (columnId: number) => {
    setTableColumns((current) => current.filter((column) => column.id !== columnId))
    setTable((current) =>
      current.map((row) => {
        const values = { ...row.values }
        delete values[columnId]
        return { ...row, values }
      })
    )
  }

  return (
    <div className="mirai-graphing-layout grid min-h-0 flex-1 grid-cols-[340px_minmax(0,1fr)] overflow-hidden @max-[699px]:grid-cols-1 @max-[699px]:overflow-y-auto">
      <GraphingSidebar
        compiled={compiled}
        rows={rows}
        tableColumns={tableColumns}
        table={table}
        tableMinimumWidth={tableMinimumWidth}
        tableSeries={tableSeries}
        formatNumber={formatNumber}
        updateRow={updateRow}
        commitRows={commitRows}
        addTableVariable={addTableVariable}
        addTableRow={addTableRow}
        setTable={setTable}
        setTableColumns={setTableColumns}
        removeTableVariable={removeTableVariable}
        updateCell={updateCell}
        addExpression={addExpression}
      />

      <GraphingCanvasViewport
        graphHostRef={graphHostRef}
        canvasRef={canvasRef}
        trace={trace}
        tracePosition={tracePosition}
        handlePointerDown={handlePointerDown}
        handlePointerMove={handlePointerMove}
        handlePointerUp={handlePointerUp}
        formatNumber={formatNumber}
        zoom={zoom}
        setView={setView}
        gridVisible={gridVisible}
        onGridVisibleChange={onGridVisibleChange}
        analysisOpen={analysisOpen}
        setAnalysisOpen={setAnalysisOpen}
        marks={marks}
        setTrace={setTrace}
      />
    </div>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/modes/scientific-key-label.tsx
import type { ScientificKeyDefinition } from "@/components/mirai-calculator/modes/scientific-keypad-config"

/** Renders structured keypad notation with the host application's mathematical font fallback. */
export function ScientificKeyLabel({ definition }: { definition: ScientificKeyDefinition }) {
  const notation = definition.notation
  if (!notation) {
    return definition.label
  }

  return (
    <span
      aria-hidden="true"
      data-math-notation=""
      className="inline-flex items-baseline font-[math,var(--font-sans,system-ui),sans-serif]! leading-none"
    >
      {notation.kind === "fraction" && (
        <span data-math-fraction="">
          {notation.numerator}⁄{notation.denominator}
        </span>
      )}
      {notation.kind === "root" && (
        <span>
          <sup className="mr-px leading-none">{notation.index}</sup>√{notation.radicand}
        </span>
      )}
      {notation.kind === "subscript" && (
        <span>
          {notation.base}
          <sub className="leading-none">{notation.script}</sub>
        </span>
      )}
      {notation.kind === "superscript" && (
        <span>
          {notation.base}
          <sup className="leading-none">{notation.script}</sup>
        </span>
      )}
    </span>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/modes/scientific-keypad.tsx
import { ScientificKeyLabel } from "@/components/mirai-calculator/modes/scientific-key-label"
import {
  SCIENTIFIC_KEY_SETS,
  getScientificKeyLabelSize,
} from "@/components/mirai-calculator/modes/scientific-keypad-config"
import type {
  ScientificKeyDefinition,
  ScientificKeypadTab,
} from "@/components/mirai-calculator/modes/scientific-keypad-config"
import { Button } from "@/components/ui/button"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { cn } from "@/lib/utils"

interface ScientificKeypadProps {
  tab: ScientificKeypadTab
  onTabChange: (tab: ScientificKeypadTab) => void
  onPressKey: (key: ScientificKeyDefinition) => void
}

/** Renders the scientific keypad tabs and their calculator keys. */
export function ScientificKeypad({ tab, onTabChange, onPressKey }: ScientificKeypadProps) {
  return (
    <Tabs
      value={tab}
      onValueChange={(value) => {
        switch (value) {
          case "basic":
          case "functions":
          case "trig":
          case "stats":
          case "variables":
            onTabChange(value)
            break
        }
      }}
      className="min-h-0 flex-1 gap-0"
    >
      <TabsList
        variant="line"
        className="mirai-keypad-tabs-list h-10! w-full flex-none justify-start gap-1 overflow-x-auto overflow-y-hidden overscroll-x-contain scroll-px-4 rounded-none border-y bg-muted/45 px-4 py-0"
      >
        <TabsTrigger
          className="mirai-keypad-tab h-10 flex-none rounded-none bg-transparent! px-3 text-sm font-semibold text-muted-foreground shadow-none! hover:bg-transparent! data-active:bg-transparent! data-active:text-primary after:right-0 after:-bottom-px! after:left-0 after:h-0.5! after:bg-primary!"
          value="basic"
        >
          Basic
        </TabsTrigger>
        <TabsTrigger
          className="mirai-keypad-tab h-10 flex-none rounded-none bg-transparent! px-3 text-sm font-semibold text-muted-foreground shadow-none! hover:bg-transparent! data-active:bg-transparent! data-active:text-primary after:right-0 after:-bottom-px! after:left-0 after:h-0.5! after:bg-primary!"
          value="functions"
        >
          Functions
        </TabsTrigger>
        <TabsTrigger
          className="mirai-keypad-tab h-10 flex-none rounded-none bg-transparent! px-3 text-sm font-semibold text-muted-foreground shadow-none! hover:bg-transparent! data-active:bg-transparent! data-active:text-primary after:right-0 after:-bottom-px! after:left-0 after:h-0.5! after:bg-primary!"
          value="trig"
        >
          Trig
        </TabsTrigger>
        <TabsTrigger
          className="mirai-keypad-tab h-10 flex-none rounded-none bg-transparent! px-3 text-sm font-semibold text-muted-foreground shadow-none! hover:bg-transparent! data-active:bg-transparent! data-active:text-primary after:right-0 after:-bottom-px! after:left-0 after:h-0.5! after:bg-primary!"
          value="stats"
        >
          Stats
        </TabsTrigger>
        <TabsTrigger
          className="mirai-keypad-tab h-10 flex-none rounded-none bg-transparent! px-3 text-sm font-semibold text-muted-foreground shadow-none! hover:bg-transparent! data-active:bg-transparent! data-active:text-primary after:right-0 after:-bottom-px! after:left-0 after:h-0.5! after:bg-primary!"
          value="variables"
        >
          Variables
        </TabsTrigger>
      </TabsList>
      <div className="grid min-h-61.5 flex-1 grid-cols-6 auto-rows-fr gap-2 p-4">
        {SCIENTIFIC_KEY_SETS[tab].map((key) => {
          const Icon = key.icon
          return (
            <Button
              key={key.ariaLabel ?? key.label}
              variant={key.tone === "primary" ? "default" : "outline"}
              onClick={() => onPressKey(key)}
              aria-label={key.ariaLabel ?? key.label}
              data-key-label-size={key.icon ? "single" : getScientificKeyLabelSize(key.label)}
              className={cn(
                "mirai-scientific-key h-auto min-h-11 min-w-0 overflow-hidden rounded-[6px] px-[clamp(0.125rem,1cqi,0.75rem)] font-sans text-sm leading-tight font-medium whitespace-nowrap data-[key-label-size=single]:text-lg data-[key-label-size=short]:text-base data-[key-label-size=medium]:text-sm data-[key-label-size=long]:px-px data-[key-label-size=long]:text-xs",
                key.tone === "operator" &&
                  "border-primary/25 bg-primary/5 text-primary hover:bg-primary/10",
                key.span === 2 && "col-span-2"
              )}
            >
              {Icon ? <Icon aria-hidden="true" /> : <ScientificKeyLabel definition={key} />}
            </Button>
          )
        })}
      </div>
    </Tabs>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/modes/scientific-keypad-config.ts
import { Delete } from "lucide-react"
import type { LucideIcon } from "lucide-react"

export interface ScientificKeyDefinition {
  label: string
  notation?: ScientificKeyNotation
  icon?: LucideIcon
  token?: string
  action?: "absolute" | "backspace" | "clear" | "evaluate" | "left" | "right"
  tone?: "default" | "operator" | "primary"
  ariaLabel?: string
  span?: 2
}

type ScientificKeyNotation =
  | { kind: "fraction"; numerator: string; denominator: string }
  | { kind: "root"; index: string; radicand: string }
  | { kind: "subscript"; base: string; script: string }
  | { kind: "superscript"; base: string; script: string }

const BASIC_KEYS: Array<ScientificKeyDefinition> = [
  { label: "(" },
  { label: ")" },
  { label: "%", ariaLabel: "Percent" },
  { label: "÷", tone: "operator", ariaLabel: "Divide" },
  {
    label: "Backspace",
    icon: Delete,
    action: "backspace",
    ariaLabel: "Backspace",
  },
  { label: "C", action: "clear", ariaLabel: "Clear" },
  { label: "7" },
  { label: "8" },
  { label: "9" },
  { label: "×", tone: "operator", ariaLabel: "Multiply" },
  { label: "x²", token: "²", notation: { kind: "superscript", base: "x", script: "2" } },
  { label: "√", token: "√(" },
  { label: "4" },
  { label: "5" },
  { label: "6" },
  { label: "−", tone: "operator", ariaLabel: "Subtract" },
  { label: "xʸ", token: "^", notation: { kind: "superscript", base: "x", script: "y" } },
  { label: "π", token: "π" },
  { label: "1" },
  { label: "2" },
  { label: "3" },
  { label: "+", tone: "operator", ariaLabel: "Add" },
  { label: "|x|", action: "absolute", ariaLabel: "Absolute value" },
  { label: "e", token: "e" },
  { label: "±", token: "−", ariaLabel: "Toggle sign" },
  { label: "0" },
  { label: "." },
  {
    label: "a⁄b",
    token: "÷",
    ariaLabel: "Fraction",
    notation: { kind: "fraction", numerator: "a", denominator: "b" },
  },
  {
    label: "=",
    action: "evaluate",
    tone: "primary",
    ariaLabel: "Equals",
    span: 2,
  },
]

const FUNCTION_KEYS: Array<ScientificKeyDefinition> = [
  { label: "x²", token: "²", notation: { kind: "superscript", base: "x", script: "2" } },
  { label: "x³", token: "³", notation: { kind: "superscript", base: "x", script: "3" } },
  { label: "xʸ", token: "^", notation: { kind: "superscript", base: "x", script: "y" } },
  { label: "√", token: "√(" },
  { label: "∛", token: "∛(" },
  {
    label: "1/x",
    token: "1÷(",
    notation: { kind: "fraction", numerator: "1", denominator: "x" },
  },
  { label: "|x|", action: "absolute", ariaLabel: "Absolute value" },
  {
    label: "10ˣ",
    token: "10^(",
    notation: { kind: "superscript", base: "10", script: "x" },
  },
  { label: "eˣ", token: "exp(", notation: { kind: "superscript", base: "e", script: "x" } },
  { label: "log", token: "log(" },
  { label: "ln", token: "ln(" },
  {
    label: "logᵦ",
    token: "logb(",
    ariaLabel: "Log base",
    notation: { kind: "subscript", base: "log", script: "b" },
  },
  {
    label: "n√x",
    token: "nthroot(",
    ariaLabel: "Nth root",
    notation: { kind: "root", index: "n", radicand: "x" },
  },
  { label: "x!", token: "!" },
  { label: "%", token: "%" },
  { label: "abs", token: "abs(" },
  { label: "floor", token: "floor(" },
  { label: "ceil", token: "ceil(" },
  { label: "round", token: "round(" },
  { label: "mod", token: "mod(" },
]

const TRIG_KEYS: Array<ScientificKeyDefinition> = [
  { label: "sin", token: "sin(" },
  { label: "cos", token: "cos(" },
  { label: "tan", token: "tan(" },
  { label: "sec", token: "sec(" },
  { label: "csc", token: "csc(" },
  { label: "cot", token: "cot(" },
  {
    label: "sin⁻¹",
    token: "asin(",
    notation: { kind: "superscript", base: "sin", script: "−1" },
  },
  {
    label: "cos⁻¹",
    token: "acos(",
    notation: { kind: "superscript", base: "cos", script: "−1" },
  },
  {
    label: "tan⁻¹",
    token: "atan(",
    notation: { kind: "superscript", base: "tan", script: "−1" },
  },
  { label: "sinh", token: "sinh(" },
  { label: "cosh", token: "cosh(" },
  { label: "tanh", token: "tanh(" },
  {
    label: "π/2",
    token: "π÷2",
    notation: { kind: "fraction", numerator: "π", denominator: "2" },
  },
  {
    label: "π/3",
    token: "π÷3",
    notation: { kind: "fraction", numerator: "π", denominator: "3" },
  },
  {
    label: "π/4",
    token: "π÷4",
    notation: { kind: "fraction", numerator: "π", denominator: "4" },
  },
  {
    label: "π/6",
    token: "π÷6",
    notation: { kind: "fraction", numerator: "π", denominator: "6" },
  },
  { label: "deg→rad", token: "×π÷180" },
  { label: "rad→deg", token: "×180÷π" },
  { label: "(", token: "(" },
  { label: ")", token: ")" },
]

const STATS_KEYS: Array<ScientificKeyDefinition> = [
  { label: "[ ]", token: "[" },
  { label: ",", token: "," },
  { label: "mean", token: "mean([" },
  { label: "median", token: "median([" },
  { label: "mode", token: "mode([" },
  { label: "Σ", token: "sum([" },
  { label: "min", token: "min([" },
  { label: "max", token: "max([" },
  { label: "range", token: "range([" },
  {
    label: "Q₁",
    token: "quartile([",
    notation: { kind: "subscript", base: "Q", script: "1" },
  },
  { label: "σ", token: "stdevp([" },
  { label: "s", token: "stdev([" },
  { label: "variance", token: "var([" },
  { label: "IQR", token: "iqr([" },
  { label: "sort", token: "sort([" },
  { label: "nCr", token: "ncr(" },
  { label: "nPr", token: "npr(" },
  { label: "gcd", token: "gcd(" },
  { label: "lcm", token: "lcm(" },
  { label: "corr", token: "corr([" },
]

const VARIABLE_KEYS: Array<ScientificKeyDefinition> = [
  { label: "a" },
  { label: "b" },
  { label: "c" },
  { label: "d" },
  { label: "x" },
  { label: "y" },
  { label: "n" },
  { label: "r" },
  { label: "t" },
  { label: "Ans", token: "Ans" },
  { label: "π", token: "π" },
  { label: "e", token: "e" },
  { label: "=", token: "=" },
  { label: "<", token: "<" },
  { label: ">", token: ">" },
  { label: "{ }", token: "{" },
  { label: ":", token: ":" },
  { label: "[ ]", token: "[" },
  { label: "(", token: "(" },
  { label: ")", token: ")" },
]

export const SCIENTIFIC_KEY_SETS = {
  basic: BASIC_KEYS,
  functions: FUNCTION_KEYS,
  trig: TRIG_KEYS,
  stats: STATS_KEYS,
  variables: VARIABLE_KEYS,
}

/** Available scientific keypad categories. */
export type ScientificKeypadTab = keyof typeof SCIENTIFIC_KEY_SETS

/** Returns a label-length bucket used to fit keypad text within compact buttons. */
export function getScientificKeyLabelSize(label: string) {
  const length = Array.from(label).length
  if (length === 1) {
    return "single"
  }
  if (length <= 3) {
    return "short"
  }
  if (length <= 5) {
    return "medium"
  }
  return "long"
}

export const SCIENTIFIC_EXAMPLES = [
  { expression: "(2÷3)+(1÷6)", label: "Add fractions" },
  { expression: "sin(30)", label: "sin(30°)" },
  { expression: "10ncr3", label: "10 choose 3" },
  { expression: "logb(2,32)", label: "log₂ 32" },
  { expression: "−3^2", label: "Order of operations" },
  { expression: "f(4)", label: "Use f(x)" },
]
../../packages/calculator-registry/src/components/mirai-calculator/modes/scientific-mode.tsx
import { useMemo, useRef, useState } from "react"
import { Redo2, Undo2 } from "lucide-react"

import { ScientificKeypad } from "@/components/mirai-calculator/modes/scientific-keypad"
import type {
  ScientificKeyDefinition,
  ScientificKeypadTab,
} from "@/components/mirai-calculator/modes/scientific-keypad-config"
import { ScientificSidebar } from "@/components/mirai-calculator/modes/scientific-sidebar"
import type { ScientificHistoryEntry } from "@/components/mirai-calculator/modes/scientific-sidebar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { completeExpression, formatExpressionInput } from "@openmirai/calculator-core/engine"
import type { CalculatorEngine, NumberFormatOptions } from "@openmirai/calculator-core/engine"
import { cn } from "@/lib/utils"

export interface ScientificModeProps {
  engine: CalculatorEngine
  definitions: Array<string>
  onDefinitionsChange: (definitions: Array<string>) => void
  onAnsChange: (answer: number) => void
  formatOptions: NumberFormatOptions
}

/** Renders the scientific expression workspace and calculator keypad. */
export function ScientificMode({
  engine,
  definitions,
  onDefinitionsChange,
  onAnsChange,
  formatOptions,
}: ScientificModeProps) {
  const [expression, setExpression] = useState("")
  const [tab, setTab] = useState<ScientificKeypadTab>("basic")
  const [history, setHistory] = useState<Array<ScientificHistoryEntry>>([])
  const [fractionView, setFractionView] = useState(false)
  const [undoStack, setUndoStack] = useState<Array<string>>([])
  const [redoStack, setRedoStack] = useState<Array<string>>([])
  const inputRef = useRef<HTMLInputElement>(null)
  const nextHistoryIdRef = useRef(0)
  const liveResult = useMemo(() => {
    if (!expression.trim()) {
      return null
    }
    try {
      const value = engine.evaluate(completeExpression(expression))
      const fraction = fractionView && typeof value === "number" ? engine.toFraction(value) : null
      return {
        value,
        text: fraction ?? formatExpressionInput(engine.format(value, formatOptions)),
        note: fraction ? "exact fraction" : "=",
        error: false,
      }
    } catch (error) {
      return {
        value: undefined,
        text: error instanceof Error ? error.message : "Invalid expression",
        note: "",
        error: true,
      }
    }
  }, [engine, expression, formatOptions, fractionView])

  const updateExpression = (next: string) => {
    setUndoStack((stack) => [expression, ...stack].slice(0, 50))
    setRedoStack([])
    setExpression(next)
  }

  const updateTypedExpression = (next: string, selectionStart: number, selectionEnd: number) => {
    const formatted = formatExpressionInput(next)
    const formattedStart = formatExpressionInput(next.slice(0, selectionStart)).length
    const formattedEnd = formatExpressionInput(next.slice(0, selectionEnd)).length
    updateExpression(formatted)
    requestAnimationFrame(() => {
      inputRef.current?.setSelectionRange(formattedStart, formattedEnd)
    })
  }

  const insert = (token: string) => {
    const input = inputRef.current
    const start = input?.selectionStart ?? expression.length
    const end = input?.selectionEnd ?? start
    const next = expression.slice(0, start) + token + expression.slice(end)
    updateExpression(next)
    requestAnimationFrame(() => {
      inputRef.current?.focus()
      inputRef.current?.setSelectionRange(start + token.length, start + token.length)
    })
  }

  const removePreviousCharacter = () => {
    const input = inputRef.current
    const start = input?.selectionStart ?? expression.length
    const end = input?.selectionEnd ?? start
    if (start === 0 && end === 0) {
      return
    }
    const next =
      start === end
        ? expression.slice(0, start - 1) + expression.slice(end)
        : expression.slice(0, start) + expression.slice(end)
    updateExpression(next)
    const cursor = start === end ? start - 1 : start
    requestAnimationFrame(() => inputRef.current?.setSelectionRange(cursor, cursor))
  }

  const insertAbsoluteValue = () => {
    const input = inputRef.current
    const start = input?.selectionStart ?? expression.length
    const end = input?.selectionEnd ?? start
    const selected = expression.slice(start, end)
    const wrapped = `|${selected}|`
    const next = expression.slice(0, start) + wrapped + expression.slice(end)
    updateExpression(next)
    const cursor = selected ? start + wrapped.length : start + 1
    requestAnimationFrame(() => {
      inputRef.current?.focus()
      inputRef.current?.setSelectionRange(cursor, cursor)
    })
  }

  const commit = () => {
    if (!expression.trim()) {
      return
    }
    const completedExpression = completeExpression(expression)
    if (completedExpression !== expression) {
      setExpression(completedExpression)
    }
    try {
      const value = engine.evaluate(completedExpression)
      const result = formatExpressionInput(engine.format(value, formatOptions))
      if (typeof value === "number") {
        onAnsChange(value)
      }
      nextHistoryIdRef.current += 1
      const id = nextHistoryIdRef.current
      setHistory((entries) => {
        const current = entries[0]
        if (
          current?.expression === completedExpression &&
          current.result === result &&
          !current.error
        ) {
          return entries
        }

        return [
          {
            id,
            expression: completedExpression,
            result,
            value: typeof value === "number" ? value : undefined,
          },
          ...entries,
        ].slice(0, 40)
      })
    } catch (error) {
      const result = error instanceof Error ? error.message : "Invalid expression"
      nextHistoryIdRef.current += 1
      const id = nextHistoryIdRef.current
      setHistory((entries) => {
        const current = entries[0]
        if (
          current?.expression === completedExpression &&
          current.result === result &&
          current.error
        ) {
          return entries
        }

        return [
          {
            id,
            expression: completedExpression,
            result,
            error: true,
          },
          ...entries,
        ].slice(0, 40)
      })
    }
  }

  const pressKey = (key: ScientificKeyDefinition) => {
    if (key.action === "absolute") {
      return insertAbsoluteValue()
    }
    if (key.action === "backspace") {
      return removePreviousCharacter()
    }
    if (key.action === "clear") {
      return updateExpression("")
    }
    if (key.action === "evaluate") {
      return commit()
    }
    insert(key.token ?? key.label)
  }

  const undo = () => {
    const previous = undoStack[0]
    if (previous === undefined) {
      return
    }
    setRedoStack((stack) => [expression, ...stack])
    setUndoStack((stack) => stack.slice(1))
    setExpression(previous)
  }

  const redo = () => {
    const next = redoStack[0]
    if (next === undefined) {
      return
    }
    setUndoStack((stack) => [expression, ...stack])
    setRedoStack((stack) => stack.slice(1))
    setExpression(next)
  }

  return (
    <div className="mirai-scientific-layout grid min-h-0 flex-1 grid-cols-[minmax(0,1.65fr)_minmax(300px,0.85fr)] overflow-hidden @max-[699px]:grid-cols-1 @max-[699px]:overflow-y-auto">
      <section className="mirai-scientific-primary flex min-h-0 min-w-0 flex-col overflow-hidden border-r @max-[699px]:min-h-112.5 @max-[699px]:border-r-0 @max-[699px]:border-b">
        <div className="flex min-h-33 shrink-0 flex-col justify-center gap-2 px-6 py-4">
          <label className="sr-only" htmlFor="mirai-expression">
            Calculator expression
          </label>
          <Input
            ref={inputRef}
            id="mirai-expression"
            value={expression}
            onChange={(event) =>
              updateTypedExpression(
                event.target.value,
                event.target.selectionStart ?? event.target.value.length,
                event.target.selectionEnd ?? event.target.value.length
              )
            }
            onKeyDown={(event) => {
              if (event.key === "Enter") {
                event.preventDefault()
                commit()
              }
            }}
            placeholder="Type or tap to start"
            autoComplete="off"
            spellCheck={false}
            className="h-auto border-0 bg-transparent px-0 text-right font-[math,var(--font-sans,system-ui),sans-serif]! text-3xl! font-semibold tracking-[-0.02em] shadow-none placeholder:font-sans focus-visible:ring-0 dark:bg-transparent sm:text-4xl!"
          />
          <div
            aria-live="polite"
            className={cn(
              "flex min-h-9 items-center justify-end gap-2 text-right",
              liveResult?.error && "text-destructive"
            )}
          >
            {liveResult?.note && (
              <span className="text-xs text-muted-foreground">{liveResult.note}</span>
            )}
            <span
              className={cn(
                "font-[math,var(--font-sans,system-ui),sans-serif]! text-2xl font-semibold tracking-[-0.015em] text-primary sm:text-3xl",
                liveResult?.error && "max-w-full text-lg leading-tight text-destructive sm:text-xl"
              )}
            >
              {liveResult?.text ?? " "}
            </span>
          </div>
          <div className="flex flex-wrap justify-end gap-2">
            <Button
              variant={fractionView ? "secondary" : "outline"}
              size="sm"
              onClick={() => setFractionView((visible) => !visible)}
            >
              {fractionView ? "Decimal" : "Fraction"}
            </Button>
            <Button variant="outline" size="sm" onClick={() => insert("Ans")}>
              Use Ans
            </Button>
            <Button
              variant="outline"
              size="icon-sm"
              onClick={undo}
              disabled={undoStack.length === 0}
              aria-label="Undo expression edit"
            >
              <Undo2 />
            </Button>
            <Button
              variant="outline"
              size="icon-sm"
              onClick={redo}
              disabled={redoStack.length === 0}
              aria-label="Redo expression edit"
            >
              <Redo2 />
            </Button>
          </div>
        </div>

        <ScientificKeypad tab={tab} onTabChange={setTab} onPressKey={pressKey} />
      </section>

      <ScientificSidebar
        definitions={definitions}
        onDefinitionsChange={onDefinitionsChange}
        history={history}
        onHistoryClear={() => setHistory([])}
        onExpressionReuse={updateExpression}
      />
    </div>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/modes/scientific-sidebar.tsx
import { useRef } from "react"
import { X } from "lucide-react"

import { CalculatorEmptyStateAction } from "@/components/mirai-calculator/calculator-empty-state-action"
import { SCIENTIFIC_EXAMPLES } from "@/components/mirai-calculator/modes/scientific-keypad-config"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import { formatExpressionInput } from "@openmirai/calculator-core/engine"
import { cn } from "@/lib/utils"

/** Completed scientific calculation shown in the local history panel. */
export interface ScientificHistoryEntry {
  id: number
  expression: string
  result: string
  value?: number
  error?: boolean
}

interface ScientificSidebarProps {
  definitions: Array<string>
  onDefinitionsChange: (definitions: Array<string>) => void
  history: Array<ScientificHistoryEntry>
  onHistoryClear: () => void
  onExpressionReuse: (expression: string) => void
}

/** Renders reusable definitions, calculation history, and scientific examples. */
export function ScientificSidebar({
  definitions,
  onDefinitionsChange,
  history,
  onHistoryClear,
  onExpressionReuse,
}: ScientificSidebarProps) {
  const definitionIdsRef = useRef<Array<number>>([])
  const nextDefinitionIdRef = useRef(0)
  while (definitionIdsRef.current.length < definitions.length) {
    nextDefinitionIdRef.current += 1
    definitionIdsRef.current.push(nextDefinitionIdRef.current)
  }
  definitionIdsRef.current.length = definitions.length

  const addDefinition = () => {
    nextDefinitionIdRef.current += 1
    definitionIdsRef.current.push(nextDefinitionIdRef.current)
    onDefinitionsChange([...definitions, ""])
  }

  const removeDefinition = (index: number) => {
    definitionIdsRef.current.splice(index, 1)
    onDefinitionsChange(definitions.filter((_, current) => current !== index))
  }

  const updateDefinition = (index: number, value: string) => {
    onDefinitionsChange(
      definitions.map((definition, current) => (current === index ? value : definition))
    )
  }

  return (
    <aside className="mirai-scientific-secondary flex min-h-0 flex-col bg-muted/25 @max-[699px]:min-h-75">
      <ScrollArea className="min-h-0 flex-1">
        <section className="border-b p-4">
          <div className="mb-3 flex items-center justify-between">
            <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
              Definitions
            </h3>
            <Button
              variant="ghost"
              size="sm"
              onClick={addDefinition}
              className="h-6 px-1.5 text-xs text-primary"
            >
              Add
            </Button>
          </div>
          <div className="space-y-2.5">
            {definitions.map((definition, index) => (
              <div className="space-y-0.5" key={definitionIdsRef.current[index]}>
                <div className="flex items-center gap-1.5">
                  <Input
                    value={definition}
                    onChange={(event) => updateDefinition(index, event.target.value)}
                    placeholder="a = 5"
                    aria-label={`Definition ${index + 1}`}
                    className="h-8 rounded-md bg-background px-2.5 font-sans text-sm font-medium"
                  />
                  <Button
                    variant="ghost"
                    size="icon-sm"
                    onClick={() => removeDefinition(index)}
                    aria-label={`Remove definition ${index + 1}`}
                    className="text-muted-foreground hover:text-destructive"
                  >
                    <X />
                  </Button>
                </div>
                <p className="pl-0.5 text-xs text-muted-foreground">
                  Reusable in calculations and graphs
                </p>
              </div>
            ))}
            {definitions.length === 0 && (
              <p className="py-2 text-xs text-muted-foreground">
                <CalculatorEmptyStateAction label="Add definition" onClick={addDefinition} /> a
                variable or function definition.
              </p>
            )}
          </div>
        </section>

        <section className="min-h-47.5 p-4">
          <div className="mb-3 flex items-center justify-between">
            <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
              History
            </h3>
            {history.length > 0 && (
              <Button
                variant="ghost"
                size="sm"
                onClick={onHistoryClear}
                className="h-6 px-1.5 text-xs text-primary"
              >
                Clear
              </Button>
            )}
          </div>
          <div className="space-y-2">
            {history.map((entry) => (
              <Button
                key={entry.id}
                variant="outline"
                onClick={() => onExpressionReuse(entry.expression)}
                className="h-auto w-full flex-col items-stretch gap-0.5 rounded-lg px-3 py-2.5 text-left"
              >
                <span className="break-all text-xs text-muted-foreground">
                  <span className="font-[math,var(--font-sans,system-ui),sans-serif]!">
                    {entry.expression}
                  </span>
                </span>
                <span
                  className={cn(
                    "font-[math,var(--font-sans,system-ui),sans-serif]! text-lg font-semibold",
                    entry.error && "text-destructive"
                  )}
                >
                  {entry.result}
                </span>
              </Button>
            ))}
            {history.length === 0 && (
              <p className="px-3 py-5 text-center text-sm leading-5 text-muted-foreground">
                Your calculations appear here. Tap one to reuse it.
              </p>
            )}
          </div>
        </section>
      </ScrollArea>

      <section className="shrink-0 border-t p-3">
        <h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
          Try this
        </h3>
        <div className="flex flex-wrap gap-1.5">
          {SCIENTIFIC_EXAMPLES.map((example) => (
            <Button
              key={example.expression}
              variant="outline"
              size="sm"
              onClick={() => onExpressionReuse(formatExpressionInput(example.expression))}
              className="h-6.5 rounded-full px-2.5 text-xs text-muted-foreground hover:border-primary hover:text-primary"
            >
              {example.label}
            </Button>
          ))}
        </div>
      </section>

      <p className="shrink-0 border-t px-4 py-3 text-xs leading-5 text-muted-foreground">
        Type with your keyboard too—<strong>sin</strong>, <strong>sqrt</strong>, <strong>^</strong>,
        Enter to evaluate.
      </p>
    </aside>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/modes/statistics-config.ts
import type { RegressionModel } from "@openmirai/calculator-core/statistics"

/** Chart variants available in the statistics workspace. */
export type StatisticsChartType = "scatter" | "histogram" | "box" | "dot" | "residual"

/** User-facing statistics chart options. */
export const STATISTICS_CHART_OPTIONS: Array<{
  value: StatisticsChartType
  label: string
}> = [
  { value: "scatter", label: "Scatter" },
  { value: "histogram", label: "Histogram" },
  { value: "box", label: "Box plot" },
  { value: "dot", label: "Dot plot" },
  { value: "residual", label: "Residuals" },
]

/** User-facing regression model options. */
export const STATISTICS_REGRESSION_OPTIONS: Array<{
  value: RegressionModel
  label: string
}> = [
  { value: "linear", label: "Linear" },
  { value: "quadratic", label: "Quadratic" },
  { value: "cubic", label: "Cubic" },
  { value: "exponential", label: "Exponential" },
  { value: "logarithmic", label: "Logarithmic" },
  { value: "power", label: "Power" },
]
../../packages/calculator-registry/src/components/mirai-calculator/modes/statistics-mode.tsx
import { useCallback, useMemo, useRef, useState } from "react"
import { BarChart3, LineChart } from "lucide-react"

import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
import { Textarea } from "@/components/ui/textarea"
import { useStatisticsChart } from "@/components/mirai-calculator/modes/use-statistics-chart"
import {
  STATISTICS_CHART_OPTIONS,
  STATISTICS_REGRESSION_OPTIONS,
} from "@/components/mirai-calculator/modes/statistics-config"
import type { StatisticsChartType } from "@/components/mirai-calculator/modes/statistics-config"
import {
  calculateStatistics,
  correlation,
  covariance,
  fitRegression,
} from "@openmirai/calculator-core/statistics"
import type { RegressionModel } from "@openmirai/calculator-core/statistics"
import {
  pairNumberSeries,
  parseNumberList,
  serializeNumberList,
} from "@openmirai/calculator-core/statistics-data"
import type { StatisticsInitialData } from "@openmirai/calculator-core/statistics-data"
import { cn } from "@/lib/utils"

export interface StatisticsModeProps {
  formatNumber: (value: number) => string
  colorScheme?: "light" | "dark"
  defaultData?: StatisticsInitialData
}

/** Renders descriptive statistics, regression, and distribution tools. */
export function StatisticsMode({
  formatNumber: formatValue,
  colorScheme = "light",
  defaultData,
}: StatisticsModeProps) {
  const [xSource, setXSource] = useState(() => serializeNumberList(defaultData?.xValues))
  const [ySource, setYSource] = useState(() => serializeNumberList(defaultData?.yValues))
  const [chart, setChart] = useState<StatisticsChartType>("scatter")
  const [model, setModel] = useState<RegressionModel>("linear")
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const canvasHostRef = useRef<HTMLDivElement>(null)
  const formatNumber = useCallback(
    (value: number) => formatValue(Number.isFinite(value) ? Number(value.toPrecision(8)) : value),
    [formatValue]
  )

  const xValues = useMemo(() => parseNumberList(xSource), [xSource])
  const yValues = useMemo(() => parseNumberList(ySource), [ySource])
  const statistics = useMemo(
    () => (xValues.length ? calculateStatistics(xValues) : null),
    [xValues]
  )
  const regression = useMemo(
    () => fitRegression(xValues, yValues, model),
    [model, xValues, yValues]
  )
  const pairedValues = useMemo(() => pairNumberSeries(xValues, yValues), [xValues, yValues])
  const { correlationValue, covarianceValue } = useMemo(() => {
    try {
      return {
        correlationValue: correlation(pairedValues.x, pairedValues.y),
        covarianceValue: covariance(pairedValues.x, pairedValues.y),
      }
    } catch {
      return { correlationValue: Number.NaN, covarianceValue: Number.NaN }
    }
  }, [pairedValues])
  const residualRows = useMemo(() => {
    const occurrences = new Map<string, number>()
    return regression.residuals.slice(0, 12).map((residual, index) => {
      const x = pairedValues.x[index]
      const y = pairedValues.y[index]
      const signature = `${x}:${y}:${residual}`
      const occurrence = (occurrences.get(signature) ?? 0) + 1
      occurrences.set(signature, occurrence)
      return { key: `${signature}:${occurrence}`, residual, x }
    })
  }, [pairedValues, regression.residuals])

  useStatisticsChart({
    canvasRef,
    canvasHostRef,
    chart,
    colorScheme,
    formatNumber,
    pairedValues,
    regression,
    statistics,
    xValues,
  })

  const statRows = statistics
    ? [
        ["n", statistics.n],
        ["Sum", statistics.sum],
        ["Mean", statistics.mean],
        ["Median", statistics.median],
        ["Mode", statistics.mode],
        ["Min", statistics.min],
        ["Q1", statistics.q1],
        ["Q3", statistics.q3],
        ["Max", statistics.max],
        ["Range", statistics.range],
        ["IQR", statistics.iqr],
        ["σ (population)", statistics.populationStandardDeviation],
        ["s (sample)", statistics.sampleStandardDeviation],
        ["σ²", statistics.populationVariance],
        ["s²", statistics.sampleVariance],
      ]
    : []

  return (
    <div className="mirai-statistics-layout grid min-h-0 flex-1 grid-cols-[340px_minmax(0,1fr)] overflow-hidden @max-[699px]:grid-cols-1 @max-[699px]:overflow-y-auto">
      <aside className="mirai-statistics-sidebar min-h-0 border-r bg-muted/25 @max-[699px]:min-h-105 @max-[699px]:border-r-0 @max-[699px]:border-b">
        <ScrollArea className="h-full">
          <div className="space-y-5 p-5">
            <div>
              <h2 className="text-sm font-semibold">Data lists</h2>
              <p className="text-xs text-muted-foreground">
                Separate values with commas, spaces, or new lines.
              </p>
            </div>
            <label
              htmlFor="mirai-statistics-x-values"
              className="block space-y-2 text-xs font-medium uppercase tracking-wide text-muted-foreground"
            >
              X values
              <Textarea
                id="mirai-statistics-x-values"
                value={xSource}
                onChange={(event) => setXSource(event.target.value)}
                aria-label="X values"
                className="min-h-24 resize-y bg-background font-mono text-sm text-foreground normal-case tracking-normal placeholder:text-muted-foreground"
              />
            </label>
            <label
              htmlFor="mirai-statistics-y-values"
              className="block space-y-2 text-xs font-medium uppercase tracking-wide text-muted-foreground"
            >
              Y values
              <Textarea
                id="mirai-statistics-y-values"
                value={ySource}
                onChange={(event) => setYSource(event.target.value)}
                aria-label="Y values"
                className="min-h-24 resize-y bg-background font-mono text-sm text-foreground normal-case tracking-normal placeholder:text-muted-foreground"
              />
            </label>
            <div>
              <h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                Chart
              </h3>
              <div className="flex flex-wrap gap-2">
                {STATISTICS_CHART_OPTIONS.map((item) => (
                  <Button
                    key={item.value}
                    size="sm"
                    variant={chart === item.value ? "default" : "outline"}
                    onClick={() => setChart(item.value)}
                  >
                    {item.label}
                  </Button>
                ))}
              </div>
            </div>
            <div>
              <h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                Regression model
              </h3>
              <div className="flex flex-wrap gap-2">
                {STATISTICS_REGRESSION_OPTIONS.map((item) => (
                  <Button
                    key={item.value}
                    size="sm"
                    variant={model === item.value ? "default" : "outline"}
                    onClick={() => setModel(item.value)}
                  >
                    {item.label}
                  </Button>
                ))}
              </div>
            </div>

            <Card>
              <CardHeader className="pb-2">
                <CardTitle className="flex items-center justify-between text-sm">
                  Model fit
                  <Badge variant={regression.ok ? "default" : "secondary"}>
                    {regression.label}
                  </Badge>
                </CardTitle>
              </CardHeader>
              <CardContent className="space-y-2">
                {regression.params.map((parameter) => (
                  <div key={parameter.label} className="flex justify-between text-sm">
                    <span className="text-muted-foreground">{parameter.label}</span>
                    <strong className="font-mono">{formatNumber(parameter.value)}</strong>
                  </div>
                ))}
                <Separator />
                <div className="flex justify-between text-sm">
                  <span className="text-muted-foreground">R²</span>
                  <strong className="font-mono text-primary">
                    {regression.ok ? formatNumber(regression.r2) : "—"}
                  </strong>
                </div>
                <div className="flex justify-between text-sm">
                  <span className="text-muted-foreground">r</span>
                  <strong className="font-mono">{formatNumber(correlationValue)}</strong>
                </div>
                <div className="flex justify-between text-sm">
                  <span className="text-muted-foreground">covariance</span>
                  <strong className="font-mono">{formatNumber(covarianceValue)}</strong>
                </div>
                <p
                  className={cn(
                    "pt-1 text-xs text-muted-foreground",
                    !regression.ok && "text-destructive"
                  )}
                >
                  {regression.message}
                </p>
              </CardContent>
            </Card>

            {regression.ok && (
              <div>
                <h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                  Residuals
                </h3>
                <div className="space-y-1">
                  {residualRows.map((row) => (
                    <div key={row.key} className="flex justify-between text-xs">
                      <span className="text-muted-foreground">x = {formatNumber(row.x)}</span>
                      <span className="font-mono">{formatNumber(row.residual)}</span>
                    </div>
                  ))}
                </div>
              </div>
            )}
          </div>
        </ScrollArea>
      </aside>

      <section className="mirai-statistics-results grid min-h-0 grid-rows-[minmax(300px,1fr)_auto] @max-[699px]:min-h-max @max-[699px]:grid-rows-[minmax(260px,1fr)_auto]">
        <div
          ref={canvasHostRef}
          className="mirai-statistics-chart relative min-h-75 @max-[699px]:min-h-65"
        >
          <canvas
            ref={canvasRef}
            className="absolute inset-0 size-full"
            aria-label={`${chart} statistics chart`}
          />
          <Badge className="absolute top-3 right-3 gap-1" variant="secondary">
            {chart === "histogram" || chart === "box" || chart === "dot" ? (
              <BarChart3 className="size-3" />
            ) : (
              <LineChart className="size-3" />
            )}
            {STATISTICS_CHART_OPTIONS.find((item) => item.value === chart)?.label}
          </Badge>
        </div>

        <div className="mirai-statistics-summary max-h-77.5 overflow-auto overscroll-contain border-t bg-muted/25 p-4 @max-[699px]:max-h-none @max-[699px]:overflow-visible">
          <div className="mb-3">
            <div>
              <h3 className="text-sm font-semibold">One-variable statistics · x list</h3>
              <p className="text-xs text-muted-foreground">{xValues.length} valid values parsed</p>
            </div>
          </div>
          <div className="mirai-statistics-grid grid grid-cols-[repeat(auto-fit,minmax(min(165px,100%),1fr))] gap-2 @max-[699px]:grid-cols-2">
            {statRows.map(([label, value]) => (
              <Card key={String(label)} className="min-w-0 gap-1 overflow-hidden py-3">
                <CardContent className="min-w-0 px-3">
                  <div className="min-h-4 wrap-break-word text-xs leading-4 text-muted-foreground">
                    {label}
                  </div>
                  <div
                    className="max-w-full overflow-hidden font-mono text-sm font-semibold text-ellipsis whitespace-nowrap"
                    title={value === null ? "none" : formatNumber(Number(value))}
                  >
                    {value === null ? "none" : formatNumber(Number(value))}
                  </div>
                </CardContent>
              </Card>
            ))}
          </div>
        </div>
      </section>
    </div>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/modes/tools-mode.tsx
import { useMemo, useState } from "react"
import { CirclePercent, DraftingCompass, MoveDiagonal2, Ratio } from "lucide-react"

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import {
  calculateCoordinates,
  calculatePercent,
  calculateRatio,
  calculateShapes,
  parseToolNumber,
  serializeToolValue,
} from "@openmirai/calculator-core/tools"
import type { ToolsInitialData } from "@openmirai/calculator-core/tools"

export interface ToolsModeProps {
  formatNumber: (value: number) => string
  defaultData?: ToolsInitialData
}

function ResultRow({ label, value }: { label: string; value: string }) {
  return (
    <div className="mirai-tools-result flex items-center justify-between gap-4 text-sm @max-[699px]:flex-col @max-[699px]:items-start @max-[699px]:gap-0.5">
      <span className="text-muted-foreground">{label}</span>
      <strong className="min-w-0 max-w-full text-right font-mono wrap-break-word @max-[699px]:text-left">
        {value}
      </strong>
    </div>
  )
}

function NumberField({
  label,
  value,
  onChange,
}: {
  label: string
  value: string
  onChange: (value: string) => void
}) {
  return (
    <label className="min-w-0 space-y-1.5 text-xs text-muted-foreground">
      {label}
      <Input
        value={value}
        onChange={(event) => onChange(event.target.value)}
        inputMode="decimal"
        className="bg-background font-mono text-sm text-foreground"
        aria-label={label}
      />
    </label>
  )
}

/** Renders focused percent, ratio, coordinate, and geometry utilities. */
export function ToolsMode({ formatNumber: formatValue, defaultData }: ToolsModeProps) {
  const [percent, setPercent] = useState(() => serializeToolValue(defaultData?.percent))
  const [percentOf, setPercentOf] = useState(() => serializeToolValue(defaultData?.percentOf))
  const [ratioLeft, setRatioLeft] = useState(() => serializeToolValue(defaultData?.ratioLeft))
  const [ratioRight, setRatioRight] = useState(() => serializeToolValue(defaultData?.ratioRight))
  const [ratioScale, setRatioScale] = useState(() => serializeToolValue(defaultData?.ratioScale))
  const [x1, setX1] = useState(() => serializeToolValue(defaultData?.x1))
  const [y1, setY1] = useState(() => serializeToolValue(defaultData?.y1))
  const [x2, setX2] = useState(() => serializeToolValue(defaultData?.x2))
  const [y2, setY2] = useState(() => serializeToolValue(defaultData?.y2))
  const [radius, setRadius] = useState(() => serializeToolValue(defaultData?.radius))
  const [base, setBase] = useState(() => serializeToolValue(defaultData?.base))
  const [height, setHeight] = useState(() => serializeToolValue(defaultData?.height))
  const [depth, setDepth] = useState(() => serializeToolValue(defaultData?.depth))
  const formatNumber = (value: number) =>
    formatValue(Number.isFinite(value) ? Number(value.toPrecision(8)) : value)

  const percentResults = useMemo(
    () => calculatePercent(parseToolNumber(percent), parseToolNumber(percentOf)),
    [percent, percentOf]
  )
  const ratioResults = useMemo(
    () =>
      calculateRatio(
        parseToolNumber(ratioLeft),
        parseToolNumber(ratioRight),
        parseToolNumber(ratioScale, 1)
      ),
    [ratioLeft, ratioRight, ratioScale]
  )
  const coordinateResults = useMemo(
    () =>
      calculateCoordinates(
        parseToolNumber(x1),
        parseToolNumber(y1),
        parseToolNumber(x2),
        parseToolNumber(y2)
      ),
    [x1, x2, y1, y2]
  )
  let lineThroughPoints: string
  switch (coordinateResults.slope) {
    case null:
      lineThroughPoints = `x = ${formatNumber(parseToolNumber(x1))}`
      break
    default: {
      const intercept = coordinateResults.intercept
      lineThroughPoints =
        intercept === null
          ? "undefined"
          : `y = ${formatNumber(coordinateResults.slope)}x ${intercept < 0 ? "−" : "+"} ${formatNumber(Math.abs(intercept))}`
    }
  }
  const shapeResults = useMemo(
    () =>
      calculateShapes(
        parseToolNumber(radius),
        parseToolNumber(base),
        parseToolNumber(height),
        parseToolNumber(depth)
      ),
    [base, depth, height, radius]
  )

  return (
    <div className="min-h-0 flex-1 overflow-auto bg-muted/25 p-4 sm:p-6">
      <div className="mirai-tools-grid grid grid-cols-[repeat(auto-fit,minmax(min(280px,100%),1fr))] gap-4">
        <Card>
          <CardHeader>
            <CardTitle className="flex items-center gap-2 text-base">
              <CirclePercent className="size-4 text-primary" />
              Percent
            </CardTitle>
            <p className="text-xs text-muted-foreground">
              Numbers in, numbers out—no worked steps.
            </p>
          </CardHeader>
          <CardContent className="space-y-4">
            <div className="mirai-tools-fields mirai-tools-fields-two grid grid-cols-2 gap-3 @max-[699px]:grid-cols-1">
              <NumberField label="Percent" value={percent} onChange={setPercent} />
              <NumberField label="Of value" value={percentOf} onChange={setPercentOf} />
            </div>
            <div className="space-y-2 rounded-lg bg-muted p-3">
              <ResultRow label="Percent of value" value={formatNumber(percentResults.portion)} />
              <ResultRow label="Increased by" value={formatNumber(percentResults.increased)} />
              <ResultRow label="Decreased by" value={formatNumber(percentResults.decreased)} />
              <ResultRow
                label="Original before increase"
                value={formatNumber(percentResults.originalBeforeIncrease)}
              />
            </div>
          </CardContent>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle className="flex items-center gap-2 text-base">
              <Ratio className="size-4 text-primary" />
              Ratio and proportion
            </CardTitle>
            <p className="text-xs text-muted-foreground">Simplify, scale, and check equivalence.</p>
          </CardHeader>
          <CardContent className="space-y-4">
            <div className="mirai-tools-fields mirai-tools-fields-three grid grid-cols-3 gap-3 @max-[699px]:grid-cols-1">
              <NumberField label="a" value={ratioLeft} onChange={setRatioLeft} />
              <NumberField label="b" value={ratioRight} onChange={setRatioRight} />
              <NumberField label="Scale" value={ratioScale} onChange={setRatioScale} />
            </div>
            <div className="space-y-2 rounded-lg bg-muted p-3">
              <ResultRow
                label="Simplified"
                value={`${formatNumber(ratioResults.simplifiedLeft)} : ${formatNumber(ratioResults.simplifiedRight)}`}
              />
              <ResultRow
                label="Scaled"
                value={`${formatNumber(ratioResults.scaledLeft)} : ${formatNumber(ratioResults.scaledRight)}`}
              />
              <ResultRow label="As decimal" value={formatNumber(ratioResults.decimal)} />
              <ResultRow
                label="gcd · lcm"
                value={`${formatNumber(ratioResults.gcd)} · ${formatNumber(ratioResults.lcm)}`}
              />
            </div>
          </CardContent>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle className="flex items-center gap-2 text-base">
              <MoveDiagonal2 className="size-4 text-primary" />
              Coordinate geometry
            </CardTitle>
            <p className="text-xs text-muted-foreground">Two points in, distance and slope out.</p>
          </CardHeader>
          <CardContent className="space-y-4">
            <div className="mirai-tools-fields mirai-tools-fields-four grid grid-cols-4 gap-2 @max-[699px]:grid-cols-1">
              <NumberField label="x₁" value={x1} onChange={setX1} />
              <NumberField label="y₁" value={y1} onChange={setY1} />
              <NumberField label="x₂" value={x2} onChange={setX2} />
              <NumberField label="y₂" value={y2} onChange={setY2} />
            </div>
            <div className="space-y-2 rounded-lg bg-muted p-3">
              <ResultRow label="Distance" value={formatNumber(coordinateResults.distance)} />
              <ResultRow
                label="Midpoint"
                value={`(${formatNumber(coordinateResults.midpoint[0])}, ${formatNumber(coordinateResults.midpoint[1])})`}
              />
              <ResultRow
                label="Slope"
                value={
                  coordinateResults.slope === null
                    ? "undefined"
                    : formatNumber(coordinateResults.slope)
                }
              />
              <ResultRow label="Line through points" value={lineThroughPoints} />
            </div>
          </CardContent>
        </Card>

        <Card className="mirai-tools-wide col-span-full">
          <CardHeader>
            <CardTitle className="flex items-center gap-2 text-base">
              <DraftingCompass className="size-4 text-primary" />
              Shape measures
            </CardTitle>
            <p className="text-xs text-muted-foreground">
              Enter dimensions and the results update as you type.
            </p>
          </CardHeader>
          <CardContent className="space-y-4">
            <div className="mirai-tools-fields mirai-tools-fields-four grid grid-cols-4 gap-3 @max-[699px]:grid-cols-1">
              <NumberField label="Radius r" value={radius} onChange={setRadius} />
              <NumberField label="Base b" value={base} onChange={setBase} />
              <NumberField label="Height h" value={height} onChange={setHeight} />
              <NumberField label="Depth d" value={depth} onChange={setDepth} />
            </div>
            <div className="mirai-tools-shape-results grid grid-cols-4 gap-3 @max-[699px]:grid-cols-1">
              {[
                ["Circle area", shapeResults.circleArea],
                ["Circumference", shapeResults.circumference],
                ["Triangle area", shapeResults.triangleArea],
                ["Prism volume", shapeResults.prismVolume],
              ].map(([label, value]) => (
                <div key={String(label)} className="rounded-lg bg-muted p-3">
                  <div className="text-xs text-muted-foreground">{label}</div>
                  <div className="font-mono text-base font-semibold">
                    {formatValue(
                      Number.isFinite(Number(value))
                        ? Number(Number(value).toPrecision(6))
                        : Number(value)
                    )}
                  </div>
                </div>
              ))}
            </div>
          </CardContent>
        </Card>

        <Card className="mirai-tools-wide col-span-full border-primary/20 bg-primary/5">
          <CardContent className="py-4">
            <p className="text-sm font-medium text-primary">Practice tip</p>
            <p className="mt-1 text-sm text-muted-foreground">
              These helpers perform arithmetic only. They do not factor, solve, or explain a
              question, keeping the workflow appropriate for timed practice.
            </p>
          </CardContent>
        </Card>
      </div>
    </div>
  )
}
../../packages/calculator-registry/src/components/mirai-calculator/modes/use-statistics-chart.ts
import { useEffect } from "react"

import { readCanvasColor, resolveCanvasColor } from "@/components/mirai-calculator/canvas-theme"
import { CALCULATOR_CHART_TOKENS } from "@/components/mirai-calculator/calculator-ui-config"
import type { StatisticsChartType } from "@/components/mirai-calculator/modes/statistics-config"
import type { DescriptiveStatistics, RegressionResult } from "@openmirai/calculator-core/statistics"
import {
  buildHistogram,
  calculateNumberExtent,
  countNumberFrequencies,
  sampleRegression,
} from "@openmirai/calculator-core/statistics-data"
import type { PairedNumberSeries } from "@openmirai/calculator-core/statistics-data"

interface UseStatisticsChartOptions {
  canvasRef: React.RefObject<HTMLCanvasElement | null>
  canvasHostRef: React.RefObject<HTMLDivElement | null>
  chart: StatisticsChartType
  colorScheme: "light" | "dark"
  formatNumber: (value: number) => string
  pairedValues: PairedNumberSeries
  regression: RegressionResult
  statistics: DescriptiveStatistics | null
  xValues: Array<number>
}

/** Draws the selected statistics visualization and redraws it when its host resizes. */
export function useStatisticsChart({
  canvasRef,
  canvasHostRef,
  chart,
  colorScheme,
  formatNumber,
  pairedValues,
  regression,
  statistics,
  xValues,
}: UseStatisticsChartOptions): void {
  useEffect(() => {
    const canvas = canvasRef.current
    const host = canvasHostRef.current
    if (!canvas || !host) {
      return
    }

    const draw = () => {
      const bounds = host.getBoundingClientRect()
      const width = Math.max(1, Math.floor(bounds.width))
      const height = Math.max(1, Math.floor(bounds.height))
      const ratio = Math.min(window.devicePixelRatio || 1, 2)
      canvas.width = width * ratio
      canvas.height = height * ratio
      canvas.style.width = `${width}px`
      canvas.style.height = `${height}px`
      const context = canvas.getContext("2d")
      if (!context) {
        return
      }
      context.setTransform(ratio, 0, 0, ratio, 0, 0)

      const background = readCanvasColor(canvas, "--background")
      const border = readCanvasColor(canvas, "--border")
      const foreground = readCanvasColor(canvas, "--foreground")
      const muted = readCanvasColor(canvas, "--muted-foreground")
      const primary = resolveCanvasColor(canvas, CALCULATOR_CHART_TOKENS[0])
      const secondary = resolveCanvasColor(canvas, CALCULATOR_CHART_TOKENS[1])
      const padding = { top: 28, right: 28, bottom: 38, left: 48 }
      const plotWidth = width - padding.left - padding.right
      const plotHeight = height - padding.top - padding.bottom

      context.clearRect(0, 0, width, height)
      context.fillStyle = background
      context.fillRect(0, 0, width, height)
      context.font = "11px system-ui, -apple-system, sans-serif"
      context.fillStyle = muted
      context.strokeStyle = border
      context.lineWidth = 1

      const drawAxes = (xRange: [number, number], yRange: [number, number]) => {
        const mapX = (value: number) =>
          padding.left + ((value - xRange[0]) / (xRange[1] - xRange[0])) * plotWidth
        const mapY = (value: number) =>
          padding.top + (1 - (value - yRange[0]) / (yRange[1] - yRange[0])) * plotHeight

        for (let index = 0; index <= 5; index += 1) {
          const x = padding.left + (index / 5) * plotWidth
          const y = padding.top + (index / 5) * plotHeight
          context.strokeStyle = border
          context.beginPath()
          context.moveTo(x, padding.top)
          context.lineTo(x, padding.top + plotHeight)
          context.stroke()
          context.beginPath()
          context.moveTo(padding.left, y)
          context.lineTo(padding.left + plotWidth, y)
          context.stroke()
          context.fillStyle = muted
          const xValue = xRange[0] + (index / 5) * (xRange[1] - xRange[0])
          const yValue = yRange[1] - (index / 5) * (yRange[1] - yRange[0])
          context.textAlign = "center"
          context.fillText(formatNumber(xValue), x, height - 14)
          context.textAlign = "right"
          context.fillText(formatNumber(yValue), padding.left - 8, y + 4)
        }
        return { mapX, mapY }
      }

      if (chart === "histogram" && xValues.length > 0) {
        const bins = buildHistogram(xValues)
        const firstBin = bins[0]
        const lastBin = bins.at(-1)
        if (!firstBin || !lastBin) {
          return
        }
        const minimum = firstBin.minimum
        const maximum = lastBin.maximum
        const { mapX, mapY } = drawAxes(
          [minimum, maximum],
          [0, Math.max(...bins.map((bin) => bin.count), 1)]
        )
        context.fillStyle = primary
        bins.forEach((bin) => {
          const x0 = mapX(bin.minimum)
          const x1 = mapX(bin.maximum)
          const y = mapY(bin.count)
          context.fillRect(x0 + 1, y, Math.max(1, x1 - x0 - 2), mapY(0) - y)
        })
      } else if (chart === "box" && statistics) {
        const [minimum, maximum] = calculateNumberExtent(xValues)
        const mapX = (value: number) =>
          padding.left + ((value - minimum) / (maximum - minimum)) * plotWidth
        const centerY = padding.top + plotHeight / 2
        context.strokeStyle = foreground
        context.lineWidth = 2
        context.beginPath()
        context.moveTo(mapX(statistics.min), centerY)
        context.lineTo(mapX(statistics.q1), centerY)
        context.moveTo(mapX(statistics.q3), centerY)
        context.lineTo(mapX(statistics.max), centerY)
        context.stroke()
        context.fillStyle = `${primary}33`
        context.fillRect(
          mapX(statistics.q1),
          centerY - 42,
          mapX(statistics.q3) - mapX(statistics.q1),
          84
        )
        context.strokeStyle = primary
        context.strokeRect(
          mapX(statistics.q1),
          centerY - 42,
          mapX(statistics.q3) - mapX(statistics.q1),
          84
        )
        for (const value of [statistics.min, statistics.median, statistics.max]) {
          context.beginPath()
          context.moveTo(mapX(value), centerY - 48)
          context.lineTo(mapX(value), centerY + 48)
          context.stroke()
          context.fillStyle = muted
          context.fillText(formatNumber(value), mapX(value) - 10, centerY + 68)
        }
      } else if (chart === "dot" && xValues.length > 0) {
        const [minimum, maximum] = calculateNumberExtent(xValues)
        const frequencies = countNumberFrequencies(xValues)
        const maxFrequency = Math.max(...frequencies.map(({ count }) => count))
        const { mapX, mapY } = drawAxes([minimum, maximum], [0, maxFrequency + 1])
        context.fillStyle = primary
        for (const { value, count } of frequencies) {
          for (let index = 1; index <= count; index += 1) {
            context.beginPath()
            context.arc(mapX(value), mapY(index), 5, 0, Math.PI * 2)
            context.fill()
          }
        }
      } else {
        const yPlotValues =
          chart === "residual" && regression.ok ? regression.residuals : pairedValues.y
        const [xmin, xmax] = calculateNumberExtent(pairedValues.x)
        const [ymin, ymax] = calculateNumberExtent(yPlotValues)
        const { mapX, mapY } = drawAxes([xmin, xmax], [ymin, ymax])
        if (chart === "residual") {
          context.strokeStyle = muted
          context.setLineDash([5, 4])
          context.beginPath()
          context.moveTo(mapX(xmin), mapY(0))
          context.lineTo(mapX(xmax), mapY(0))
          context.stroke()
          context.setLineDash([])
        } else if (regression.ok) {
          context.strokeStyle = secondary
          context.lineWidth = 2
          context.beginPath()
          sampleRegression(regression.predict, xmin, xmax).forEach(({ x, y }, index) => {
            if (index === 0) {
              context.moveTo(mapX(x), mapY(y))
            } else {
              context.lineTo(mapX(x), mapY(y))
            }
          })
          context.stroke()
        }

        context.fillStyle = primary
        pairedValues.x.forEach((x, index) => {
          const y =
            chart === "residual" && regression.ok
              ? regression.residuals[index]
              : pairedValues.y[index]
          if (!Number.isFinite(y)) {
            return
          }
          context.beginPath()
          context.arc(mapX(x), mapY(y), 4.5, 0, Math.PI * 2)
          context.fill()
        })
      }
    }

    draw()
    const observer = new ResizeObserver(draw)
    observer.observe(host)
    return () => observer.disconnect()
  }, [
    canvasHostRef,
    canvasRef,
    chart,
    colorScheme,
    formatNumber,
    pairedValues,
    regression,
    statistics,
    xValues,
  ])
}
../../packages/calculator-registry/src/components/mirai-calculator/use-system-dark-mode.ts
import { useSyncExternalStore } from "react"

import type { CalculatorTheme } from "@openmirai/calculator-core/configuration"

/** Subscribes to changes in the browser's preferred color scheme. */
function subscribeToSystemTheme(onStoreChange: () => void): () => void {
  const query = window.matchMedia("(prefers-color-scheme: dark)")
  query.addEventListener("change", onStoreChange)
  return () => query.removeEventListener("change", onStoreChange)
}

/** Reads whether the browser currently prefers a dark color scheme. */
function getSystemThemeSnapshot(): boolean {
  return window.matchMedia("(prefers-color-scheme: dark)").matches
}

/** Supplies the deterministic light-mode server snapshot. */
function getServerSystemThemeSnapshot(): boolean {
  return false
}

/** Returns whether the host system currently prefers a dark color scheme. */
export function useSystemDarkMode(): boolean {
  return useSyncExternalStore(
    subscribeToSystemTheme,
    getSystemThemeSnapshot,
    getServerSystemThemeSnapshot
  )
}

/** Resolves the configured calculator theme against the current system preference. */
export function resolveCalculatorTheme(
  theme: CalculatorTheme,
  systemDark: boolean
): "light" | "dark" {
  switch (theme) {
    case "system":
      return systemDark ? "dark" : "light"
    case "light":
    case "dark":
      return theme
  }
}