{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thinking-select",
  "title": "ThinkingSelect",
  "description": "Reasoning-effort selector: Off, Low, Medium, High.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/elastic-slider.json",
    "https://ui.neon.com/r/neon-tokens.json"
  ],
  "files": [
    {
      "path": "src/components/thinking-select/thinking-select.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { useEffect, useRef } from \"react\";\n\nimport { ElasticSlider } from \"@/components/ui/elastic-slider\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ThinkingEffort =\n  | \"off\"\n  | \"low\"\n  | \"medium\"\n  | \"high\"\n  | \"xhigh\"\n  | \"max\";\n\nexport type ThinkingSelectSize = \"sm\" | \"md\" | \"lg\";\n\nexport type ThinkingSelectProps = Omit<\n  ComponentProps<\"div\">,\n  \"onChange\" | \"children\"\n> & {\n  /** Controlled effort level. */\n  value?: ThinkingEffort;\n  /** Uncontrolled initial effort level. */\n  defaultValue?: ThinkingEffort;\n  onValueChange?: (value: ThinkingEffort) => void;\n  disabled?: boolean;\n  /** Track size: compact, default, or roomy. */\n  size?: ThinkingSelectSize;\n};\n\n/* ─────────────────────────────────────────────────────────\n * SPARKLE STORYBOARD (per effort level, looping)\n *\n *  off     fill clean, no sparkle\n *  low     a few pixels twinkle slowly\n *  medium  more frequent twinkles\n *  high    lively, quick sparkle\n *\n *  Each sparkle is one pixel cell fading in and out on its\n *  own sine phase — a glint, not static. The layer rides the\n *  elastic fill: it reads the fill's live width every frame,\n *  so it stretches and springs with drag and snap-back.\n * ───────────────────────────────────────────────────────── */\nconst EFFORTS: ThinkingEffort[] = [\n  \"off\",\n  \"low\",\n  \"medium\",\n  \"high\",\n  \"xhigh\",\n  \"max\",\n];\n\nconst EFFORT_LABELS: Record<ThinkingEffort, string> = {\n  high: \"high\",\n  low: \"low\",\n  max: \"max\",\n  medium: \"med\",\n  off: \"off\",\n  xhigh: \"xhigh\",\n};\n\nconst SPARKLE: Record<ThinkingEffort, { density: number; speed: number }> = {\n  high: { density: 0.1, speed: 24 },\n  low: { density: 0.04, speed: 8 },\n  max: { density: 0.2, speed: 48 },\n  medium: { density: 0.07, speed: 14 },\n  off: { density: 0, speed: 0 },\n  xhigh: { density: 0.14, speed: 34 },\n};\n\nconst GRAIN_CELL = 3;\n\nconst SIZE_CLASSES: Record<ThinkingSelectSize, string> = {\n  lg: \"[--elastic-slider-height:--spacing(11)]\",\n  md: \"\",\n  sm: \"[--elastic-slider-height:--spacing(7)] [&_[data-slot=elastic-slider-label]]:text-xs/none [&_[data-slot=elastic-slider-value]]:text-xs/none\",\n};\n\nconst SIZE_MIN_WIDTH: Record<ThinkingSelectSize, string> = {\n  lg: \"min-w-52\",\n  md: \"min-w-44\",\n  sm: \"min-w-36\",\n};\n\n/** Deterministic pseudo-random noise, stable for a given cell and tick. */\nconst hashNoise = (cell: number, tick: number) => {\n  const raw = Math.sin(cell * 127.1 + tick * 311.7) * 43_758.5453;\n  return raw - Math.floor(raw);\n};\n\n/**\n * Loader-style pixel sparkle confined to the slider's fill. Reads the fill's\n * live width each frame so the glints stretch with the elastic drag and\n * spring with the snap-back.\n */\nconst GrainFill = ({ effort }: { effort: ThinkingEffort }) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const effortRef = useRef(effort);\n\n  useEffect(() => {\n    effortRef.current = effort;\n  }, [effort]);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n\n    if (!canvas) {\n      return;\n    }\n\n    const context = canvas.getContext(\"2d\");\n\n    if (!context) {\n      return;\n    }\n\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n      return;\n    }\n\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    let frame = 0;\n\n    const draw = (now: number) => {\n      frame = requestAnimationFrame(draw);\n\n      // Skip canvas work while hidden — the loop stays alive for free.\n      if (!(canvas.checkVisibility?.() ?? true)) {\n        return;\n      }\n\n      const host = canvas.parentElement;\n      const fill = host?.querySelector<HTMLElement>(\n        '[data-slot=\"elastic-slider-fill\"]'\n      );\n\n      if (!(host && fill)) {\n        return;\n      }\n\n      const hostRect = host.getBoundingClientRect();\n      const fillRect = fill.getBoundingClientRect();\n      const width = Math.round(hostRect.width * dpr);\n      const height = Math.round(hostRect.height * dpr);\n\n      if (canvas.width !== width || canvas.height !== height) {\n        canvas.width = width;\n        canvas.height = height;\n      }\n\n      context.clearRect(0, 0, width, height);\n\n      const { density, speed } = SPARKLE[effortRef.current];\n\n      if (density === 0) {\n        return;\n      }\n\n      const fillLeft = (fillRect.left - hostRect.left) * dpr;\n      const fillWidth = fillRect.width * dpr;\n      const cell = GRAIN_CELL * dpr;\n      const columns = Math.ceil(fillWidth / cell);\n      const rows = Math.ceil(height / cell);\n      const seconds = now / 1000;\n      const primary = getComputedStyle(canvas).color;\n\n      context.fillStyle = primary;\n\n      for (let index = 0; index < columns * rows; index += 1) {\n        // Sparse, stable subset of cells may ever sparkle.\n        if (hashNoise(index, 1) > density) {\n          continue;\n        }\n\n        // Each sparkle breathes on its own sine phase; it spends most\n        // of the cycle dark and glints briefly at the crest.\n        const phase = hashNoise(index, 2) * Math.PI * 2;\n        const twinkle = Math.sin(seconds * (speed / 4) + phase);\n\n        if (twinkle < 0.75) {\n          continue;\n        }\n\n        const x = fillLeft + (index % columns) * cell;\n        const y = Math.floor(index / columns) * cell;\n\n        context.globalAlpha = ((twinkle - 0.75) / 0.25) * 0.45;\n        context.fillRect(x, y, cell + 0.5, cell + 0.5);\n      }\n\n      context.globalAlpha = 1;\n    };\n\n    frame = requestAnimationFrame(draw);\n\n    return () => cancelAnimationFrame(frame);\n  }, []);\n\n  return (\n    <canvas\n      aria-hidden=\"true\"\n      className=\"pointer-events-none absolute inset-0 z-10 h-full w-full text-primary\"\n      ref={canvasRef}\n    />\n  );\n};\n\nconst effortFromIndex = (index: number): ThinkingEffort =>\n  EFFORTS[Math.round(index)] ?? \"off\";\n\nexport const ThinkingSelect = ({\n  className,\n  defaultValue = \"off\",\n  disabled = false,\n  onValueChange,\n  size = \"md\",\n  value,\n  ...props\n}: ThinkingSelectProps) => {\n  const current = value ?? defaultValue;\n\n  return (\n    <div\n      aria-disabled={disabled || undefined}\n      className={cn(\n        \"relative\",\n        SIZE_MIN_WIDTH[size],\n        disabled && \"pointer-events-none opacity-50\",\n        className\n      )}\n      data-effort={current}\n      data-slot=\"thinking-select\"\n      {...props}\n    >\n      <GrainFill effort={current} />\n      <ElasticSlider\n        aria-label=\"Thinking effort\"\n        className={cn(\n          \"w-full\",\n          SIZE_CLASSES[size],\n          \"[--elastic-slider-bg:var(--muted)]\",\n          \"[--elastic-slider-fill:color-mix(in_srgb,var(--primary)_14%,transparent)]\",\n          \"[--elastic-slider-fill-active:color-mix(in_srgb,var(--primary)_24%,transparent)]\",\n          \"[--elastic-slider-focus:var(--primary)]\",\n          \"[--elastic-slider-handle:var(--primary)]\",\n          \"[--elastic-slider-hash:color-mix(in_srgb,var(--primary)_45%,transparent)]\",\n          \"[--elastic-slider-radius:0px]\",\n          disabled && \"pointer-events-none\"\n        )}\n        formatValue={(index) => EFFORT_LABELS[effortFromIndex(index)]}\n        label=\"Thinking\"\n        max={EFFORTS.length - 1}\n        min={0}\n        onValueChange={(index) => onValueChange?.(effortFromIndex(index))}\n        step={1}\n        value={EFFORTS.indexOf(current)}\n      />\n    </div>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}