{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "parameter-slider",
  "type": "registry:block",
  "title": "Parameter Slider",
  "description": "Numeric parameter adjustment controls.",
  "dependencies": [
    "@radix-ui/react-slider",
    "zod"
  ],
  "registryDependencies": [
    "button",
    "separator",
    "slider"
  ],
  "files": [
    {
      "path": "components/tool-ui/parameter-slider/_adapter.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/parameter-slider/_adapter.tsx",
      "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn        → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button    → shadcn/ui Button\n *   Separator → shadcn/ui Separator\n *   Slider    → shadcn/ui Slider\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Separator } from \"@/components/ui/separator\";\nexport { Slider } from \"@/components/ui/slider\";\n"
    },
    {
      "path": "components/tool-ui/parameter-slider/index.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/parameter-slider/index.tsx",
      "content": "export { ParameterSlider } from \"./parameter-slider\";\nexport type {\n  ParameterSliderProps,\n  SliderConfig,\n  SliderValue,\n  SerializableParameterSlider,\n} from \"./schema\";\n"
    },
    {
      "path": "components/tool-ui/parameter-slider/math.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/parameter-slider/math.ts",
      "content": "import type { SliderConfig, SliderValue } from \"./schema\";\n\ntype SliderPercentInput = {\n  value: number;\n  min: number;\n  max: number;\n};\n\nfunction clampPercent(value: number): number {\n  if (!Number.isFinite(value)) return 0;\n  return Math.max(0, Math.min(100, value));\n}\n\nexport function sliderRangeToPercent({\n  value,\n  min,\n  max,\n}: SliderPercentInput): number {\n  const range = max - min;\n  if (!Number.isFinite(range) || range <= 0) return 0;\n  return clampPercent(((value - min) / range) * 100);\n}\n\nexport function createSliderValueSnapshot(\n  sliders: SliderConfig[],\n): SliderValue[] {\n  return sliders.map((slider) => ({ id: slider.id, value: slider.value }));\n}\n\nexport function createSliderSignature(sliders: SliderConfig[]): string {\n  return JSON.stringify(\n    sliders.map(({ id, min, max, step, value, unit, precision }) => ({\n      id,\n      min,\n      max,\n      step: step ?? 1,\n      value,\n      unit: unit ?? \"\",\n      precision: precision ?? null,\n    })),\n  );\n}\n"
    },
    {
      "path": "components/tool-ui/parameter-slider/parameter-slider.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/parameter-slider/parameter-slider.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\nimport type { ParameterSliderProps, SliderConfig, SliderValue } from \"./schema\";\nimport { ActionButtons } from \"../shared/action-buttons\";\nimport { normalizeActionsConfig } from \"../shared/actions-config\";\nimport { useControllableState } from \"../shared/use-controllable-state\";\nimport { useSignatureReset } from \"../shared/use-signature-reset\";\n\nimport { cn } from \"./_adapter\";\nimport {\n  createSliderSignature,\n  createSliderValueSnapshot,\n  sliderRangeToPercent,\n} from \"./math\";\n\nfunction formatSignedValue(\n  value: number,\n  min: number,\n  max: number,\n  precision?: number,\n  unit?: string,\n): string {\n  const crossesZero = min < 0 && max > 0;\n  const fixed =\n    precision !== undefined ? value.toFixed(precision) : String(value);\n  const numericPart = crossesZero && value >= 0 ? `+${fixed}` : fixed;\n  return unit ? `${numericPart} ${unit}` : numericPart;\n}\n\nfunction getAriaValueText(\n  value: number,\n  min: number,\n  max: number,\n  unit?: string,\n): string {\n  const crossesZero = min < 0 && max > 0;\n  if (crossesZero) {\n    if (value > 0) {\n      return unit ? `plus ${value} ${unit}` : `plus ${value}`;\n    } else if (value < 0) {\n      return unit\n        ? `minus ${Math.abs(value)} ${unit}`\n        : `minus ${Math.abs(value)}`;\n    }\n  }\n  return unit ? `${value} ${unit}` : String(value);\n}\n\nconst TICK_COUNT = 16;\nconst TEXT_PADDING_X = 4;\nconst TEXT_PADDING_X_OUTER = 0; // Less inset on outer-facing side (near edges)\nconst TEXT_PADDING_Y = 2;\nconst DETECTION_MARGIN_X = 12;\nconst DETECTION_MARGIN_X_OUTER = 4; // Small margin at edges for steep falloff - segments fully close at terminal positions\nconst DETECTION_MARGIN_Y = 12;\nconst TRACK_HEIGHT = 48;\nconst TEXT_RELEASE_INSET = 8;\nconst TRACK_EDGE_INSET = 4; // px from track edge - keeps elements visible at extremes\nconst THUMB_WIDTH = 12; // w-3\n// Text vertical offset: raised slightly from center\n// Positive = raised, negative = lowered\nconst TEXT_VERTICAL_OFFSET = 0.5;\n\nfunction clampPercent(value: number): number {\n  if (!Number.isFinite(value)) return 0;\n  return Math.max(0, Math.min(100, value));\n}\n\n// Convert a percentage (0-100) to an inset position string\n// At 0%: 4px from left edge; at 100%: 4px from right edge\nfunction toInsetPosition(percent: number): string {\n  const safePercent = clampPercent(percent);\n  return `calc(${TRACK_EDGE_INSET}px + (100% - ${TRACK_EDGE_INSET * 2}px) * ${safePercent / 100})`;\n}\n\n// Radix keeps the thumb in bounds by applying a percent-dependent px offset.\n// Matching this for fill clipping prevents handle/fill drift near extremes.\nfunction getRadixThumbInBoundsOffsetPx(percent: number): number {\n  const safePercent = clampPercent(percent);\n  const halfWidth = THUMB_WIDTH / 2;\n  return halfWidth - (safePercent * halfWidth) / 50;\n}\n\nfunction toRadixThumbPosition(percent: number): string {\n  const safePercent = clampPercent(percent);\n  const offsetPx = getRadixThumbInBoundsOffsetPx(safePercent);\n  return `calc(${safePercent}% + ${offsetPx}px)`;\n}\n\nfunction signedDistanceToRoundedRect(\n  px: number,\n  py: number,\n  left: number,\n  right: number,\n  top: number,\n  bottom: number,\n  radiusLeft: number,\n  radiusRight: number,\n): number {\n  const innerLeft = left + radiusLeft;\n  const innerRight = right - radiusRight;\n  const innerTop = top + Math.max(radiusLeft, radiusRight);\n  const innerBottom = bottom - Math.max(radiusLeft, radiusRight);\n\n  const inLeftCorner = px < innerLeft;\n  const inRightCorner = px > innerRight;\n  const inCornerY = py < innerTop || py > innerBottom;\n\n  if ((inLeftCorner || inRightCorner) && inCornerY) {\n    const radius = inLeftCorner ? radiusLeft : radiusRight;\n    const cornerX = inLeftCorner ? innerLeft : innerRight;\n    const cornerY = py < innerTop ? top + radius : bottom - radius;\n    const distToCornerCenter = Math.hypot(px - cornerX, py - cornerY);\n    return distToCornerCenter - radius;\n  }\n\n  const dx = Math.max(left - px, px - right, 0);\n  const dy = Math.max(top - py, py - bottom, 0);\n\n  if (dx === 0 && dy === 0) {\n    return -Math.min(px - left, right - px, py - top, bottom - py);\n  }\n\n  return Math.max(dx, dy);\n}\n\nconst OUTER_EDGE_RADIUS_FACTOR = 0.3; // Reduced radius on outer-facing sides for steeper falloff\n\nfunction calculateGap(\n  thumbCenterX: number,\n  textRect: { left: number; right: number; height: number; centerY: number },\n  isLeftAligned: boolean,\n): number {\n  const { left, right, height, centerY } = textRect;\n  // Asymmetric padding/margin: outer-facing side has less padding, more margin\n  const paddingLeft = isLeftAligned ? TEXT_PADDING_X_OUTER : TEXT_PADDING_X;\n  const paddingRight = isLeftAligned ? TEXT_PADDING_X : TEXT_PADDING_X_OUTER;\n  const marginLeft = isLeftAligned\n    ? DETECTION_MARGIN_X_OUTER\n    : DETECTION_MARGIN_X;\n  const marginRight = isLeftAligned\n    ? DETECTION_MARGIN_X\n    : DETECTION_MARGIN_X_OUTER;\n  const paddingY = TEXT_PADDING_Y;\n  const marginY = DETECTION_MARGIN_Y;\n  const thumbCenterY = centerY;\n\n  // Inner boundary (where max gap occurs)\n  const innerLeft = left - paddingLeft;\n  const innerRight = right + paddingRight;\n  const innerTop = centerY - height / 2 - paddingY;\n  const innerBottom = centerY + height / 2 + paddingY;\n  const innerHeight = height + paddingY * 2;\n  const innerRadius = innerHeight / 2;\n  // Smaller radius on outer-facing side (left for label, right for value)\n  const innerRadiusLeft = isLeftAligned\n    ? innerRadius * OUTER_EDGE_RADIUS_FACTOR\n    : innerRadius;\n  const innerRadiusRight = isLeftAligned\n    ? innerRadius\n    : innerRadius * OUTER_EDGE_RADIUS_FACTOR;\n\n  // Outer boundary (where effect starts) - proportionally larger\n  const outerLeft = left - paddingLeft - marginLeft;\n  const outerRight = right + paddingRight + marginRight;\n  const outerTop = centerY - height / 2 - paddingY - marginY;\n  const outerBottom = centerY + height / 2 + paddingY + marginY;\n  const outerHeight = height + paddingY * 2 + marginY * 2;\n  const outerRadius = outerHeight / 2;\n  const outerRadiusLeft = isLeftAligned\n    ? outerRadius * OUTER_EDGE_RADIUS_FACTOR\n    : outerRadius;\n  const outerRadiusRight = isLeftAligned\n    ? outerRadius\n    : outerRadius * OUTER_EDGE_RADIUS_FACTOR;\n\n  const outerDist = signedDistanceToRoundedRect(\n    thumbCenterX,\n    thumbCenterY,\n    outerLeft,\n    outerRight,\n    outerTop,\n    outerBottom,\n    outerRadiusLeft,\n    outerRadiusRight,\n  );\n\n  // Outside outer boundary - no gap\n  if (outerDist > 0) return 0;\n\n  const innerDist = signedDistanceToRoundedRect(\n    thumbCenterX,\n    thumbCenterY,\n    innerLeft,\n    innerRight,\n    innerTop,\n    innerBottom,\n    innerRadiusLeft,\n    innerRadiusRight,\n  );\n\n  // Inside inner boundary - max gap\n  const maxGap = height + paddingY * 2;\n  if (innerDist <= 0) return maxGap;\n\n  // Between boundaries - linear interpolation\n  // outerDist is negative (inside outer), innerDist is positive (outside inner)\n  const totalDist = Math.abs(outerDist) + innerDist;\n  const t = Math.abs(outerDist) / totalDist;\n\n  return maxGap * t;\n}\n\ninterface SliderRowProps {\n  config: SliderConfig;\n  value: number;\n  onChange: (value: number) => void;\n  trackClassName?: string;\n  fillClassName?: string;\n  handleClassName?: string;\n}\n\nfunction SliderRow({\n  config,\n  value,\n  onChange,\n  trackClassName,\n  fillClassName,\n  handleClassName,\n}: SliderRowProps) {\n  const { id, label, min, max, step = 1, unit, precision, disabled } = config;\n  // Per-slider theming overrides component-level theming\n  const resolvedTrackClassName = config.trackClassName ?? trackClassName;\n  const resolvedFillClassName = config.fillClassName ?? fillClassName;\n  const resolvedHandleClassName = config.handleClassName ?? handleClassName;\n  const crossesZero = min < 0 && max > 0;\n  const [isDragging, setIsDragging] = useState(false);\n  const [isHovered, setIsHovered] = useState(false);\n\n  const trackRef = useRef<HTMLSpanElement>(null);\n  const labelRef = useRef<HTMLSpanElement>(null);\n  const valueRef = useRef<HTMLSpanElement>(null);\n\n  const [dragGap, setDragGap] = useState(0);\n  const [fullGap, setFullGap] = useState(0);\n  const [intersectsText, setIntersectsText] = useState(false);\n  const [layoutVersion, setLayoutVersion] = useState(0);\n\n  useEffect(() => {\n    if (!isDragging) return;\n    const handlePointerUp = () => setIsDragging(false);\n    document.addEventListener(\"pointerup\", handlePointerUp);\n    return () => document.removeEventListener(\"pointerup\", handlePointerUp);\n  }, [isDragging]);\n\n  useEffect(() => {\n    const track = trackRef.current;\n    const labelEl = labelRef.current;\n    const valueEl = valueRef.current;\n    if (!track || !labelEl || !valueEl) return;\n\n    const bumpLayoutVersion = () => setLayoutVersion((v) => v + 1);\n\n    if (typeof ResizeObserver !== \"undefined\") {\n      const observer = new ResizeObserver(() => {\n        bumpLayoutVersion();\n      });\n      observer.observe(track);\n      observer.observe(labelEl);\n      observer.observe(valueEl);\n      return () => observer.disconnect();\n    }\n\n    window.addEventListener(\"resize\", bumpLayoutVersion);\n    return () => window.removeEventListener(\"resize\", bumpLayoutVersion);\n  }, []);\n\n  useLayoutEffect(() => {\n    const track = trackRef.current;\n    const labelEl = labelRef.current;\n    const valueEl = valueRef.current;\n\n    if (!track || !labelEl || !valueEl) return;\n\n    const trackRect = track.getBoundingClientRect();\n    const labelRect = labelEl.getBoundingClientRect();\n    const valueRect = valueEl.getBoundingClientRect();\n\n    const trackWidth = trackRect.width;\n    const valuePercent = sliderRangeToPercent({ value, min, max });\n    // Use same inset coordinate system as visual elements\n    const thumbCenterPx =\n      (trackWidth * clampPercent(valuePercent)) / 100 +\n      getRadixThumbInBoundsOffsetPx(valuePercent);\n    const thumbHalfWidth = THUMB_WIDTH / 2;\n\n    // Text is raised by TEXT_VERTICAL_OFFSET from center\n    const trackCenterY = TRACK_HEIGHT / 2 - TEXT_VERTICAL_OFFSET;\n\n    const labelGap = calculateGap(\n      thumbCenterPx,\n      {\n        left: labelRect.left - trackRect.left,\n        right: labelRect.right - trackRect.left,\n        height: labelRect.height,\n        centerY: trackCenterY,\n      },\n      true,\n    ); // label is left-aligned\n\n    const valueGap = calculateGap(\n      thumbCenterPx,\n      {\n        left: valueRect.left - trackRect.left,\n        right: valueRect.right - trackRect.left,\n        height: valueRect.height,\n        centerY: trackCenterY,\n      },\n      false,\n    ); // value is right-aligned\n\n    setDragGap(Math.max(labelGap, valueGap));\n\n    // Tight intersection check for release state\n    // Inset by px-2 (8px) padding to check against actual text, not padded container\n    const labelLeft = labelRect.left - trackRect.left + TEXT_RELEASE_INSET;\n    const labelRight = labelRect.right - trackRect.left - TEXT_RELEASE_INSET;\n    const valueLeft = valueRect.left - trackRect.left + TEXT_RELEASE_INSET;\n    const valueRight = valueRect.right - trackRect.left - TEXT_RELEASE_INSET;\n\n    const thumbLeft = thumbCenterPx - thumbHalfWidth;\n    const thumbRight = thumbCenterPx + thumbHalfWidth;\n\n    const hitsLabel = thumbRight > labelLeft && thumbLeft < labelRight;\n    const hitsValue = thumbRight > valueLeft && thumbLeft < valueRight;\n\n    setIntersectsText(hitsLabel || hitsValue);\n\n    // Calculate full separation gap for release state\n    // Use the max gap of whichever text element(s) the handle intersects\n    const labelFullGap = labelRect.height + TEXT_PADDING_Y * 2;\n    const valueFullGap = valueRect.height + TEXT_PADDING_Y * 2;\n    const releaseGap =\n      hitsLabel && hitsValue\n        ? Math.max(labelFullGap, valueFullGap)\n        : hitsLabel\n          ? labelFullGap\n          : hitsValue\n            ? valueFullGap\n            : 0;\n    setFullGap(releaseGap);\n  }, [value, min, max, layoutVersion]);\n\n  // While dragging: use distance-based separation, but never collapse below\n  // the release split when the thumb still intersects text.\n  const gap = isDragging\n    ? Math.max(dragGap, intersectsText ? fullGap : 0)\n    : intersectsText\n      ? fullGap\n      : 0;\n\n  const ticks = useMemo(() => {\n    // Generate equidistant ticks regardless of step value\n    const majorTickCount = TICK_COUNT;\n    const result: { percent: number; isCenter: boolean; isSubtick: boolean }[] =\n      [];\n\n    for (let i = 0; i <= majorTickCount; i++) {\n      const percent = (i / majorTickCount) * 100;\n      const isCenter = !crossesZero && percent === 50;\n\n      // Skip the center tick (50%) for crossesZero sliders\n      if (crossesZero && percent === 50) continue;\n\n      // Add subtick at midpoint before this tick (except for first)\n      if (i > 0) {\n        const prevPercent = ((i - 1) / majorTickCount) * 100;\n        // Don't add subtick if it would be at 50% for crossesZero\n        const midPercent = (prevPercent + percent) / 2;\n        if (!(crossesZero && midPercent === 50)) {\n          result.push({\n            percent: midPercent,\n            isCenter: false,\n            isSubtick: true,\n          });\n        }\n      }\n\n      result.push({ percent, isCenter, isSubtick: false });\n    }\n\n    return result;\n  }, [crossesZero]);\n\n  const zeroPercent = crossesZero\n    ? sliderRangeToPercent({ value: 0, min, max })\n    : 0;\n  const valuePercent = sliderRangeToPercent({ value, min, max });\n\n  // Fill clip-path uses the same inset coordinate system as the handle.\n  // This keeps the collapsed stroke aligned with the fill edge near extremes.\n  const fillClipPath = useMemo(() => {\n    const toClipFromRightInset = (percent: number) =>\n      `calc(100% - ${toRadixThumbPosition(percent)})`;\n    const toClipFromLeftInset = (percent: number) =>\n      toRadixThumbPosition(percent);\n    const TERMINAL_EPSILON = 1e-6;\n    const snapLeftInset = (percent: number) => {\n      if (percent <= TERMINAL_EPSILON) return \"0\";\n      if (percent >= 100 - TERMINAL_EPSILON) return \"100%\";\n      return toClipFromLeftInset(percent);\n    };\n    const snapRightInset = (percent: number) => {\n      if (percent <= TERMINAL_EPSILON) return \"100%\";\n      if (percent >= 100 - TERMINAL_EPSILON) return \"0\";\n      return toClipFromRightInset(percent);\n    };\n\n    if (crossesZero) {\n      // Keep center anchor stable by always clipping the low/high pair,\n      // independent of sign branch, then snapping at terminal edges.\n      const lowPercent = Math.min(valuePercent, zeroPercent);\n      const highPercent = Math.max(valuePercent, zeroPercent);\n      return `inset(0 ${snapRightInset(highPercent)} 0 ${snapLeftInset(lowPercent)})`;\n    }\n    // Non-crossing: fill starts at left edge; snap right inset at terminals.\n    return `inset(0 ${snapRightInset(valuePercent)} 0 0)`;\n  }, [crossesZero, zeroPercent, valuePercent]);\n\n  const fillMaskImage = crossesZero\n    ? \"linear-gradient(to right, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.35) 50%, rgba(0,0,0,0.7) 100%)\"\n    : \"linear-gradient(to right, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%)\";\n\n  // Metallic reflection gradient that follows the handle position\n  // Visible while dragging OR when resting at edges (0%/100%)\n  const reflectionStyle = useMemo(() => {\n    const edgeThreshold = 3;\n    const nearEdge =\n      valuePercent <= edgeThreshold || valuePercent >= 100 - edgeThreshold;\n\n    // Narrower spread when stationary at edges (~35% narrower)\n    const spreadPercent = nearEdge && !isDragging ? 6.5 : 10;\n    const handlePos = toRadixThumbPosition(valuePercent);\n    const start = `clamp(0%, calc(${handlePos} - ${spreadPercent}%), 100%)`;\n    const end = `clamp(0%, calc(${handlePos} + ${spreadPercent}%), 100%)`;\n\n    const gradient = `linear-gradient(to right,\n      transparent ${start},\n      white ${handlePos},\n      transparent ${end})`;\n\n    return {\n      background: gradient,\n      WebkitMask:\n        \"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)\",\n      WebkitMaskComposite: \"xor\",\n      maskComposite: \"exclude\",\n      padding: \"1px\",\n    };\n  }, [valuePercent, isDragging]);\n\n  // Opacity scales with handle size: rest → hover → drag\n  const reflectionOpacity = useMemo(() => {\n    const edgeThreshold = 3;\n    const atEdge =\n      valuePercent <= edgeThreshold || valuePercent >= 100 - edgeThreshold;\n\n    if (isDragging || atEdge) {\n      return 1;\n    }\n    if (isHovered) {\n      return 0.6;\n    }\n    return 0;\n  }, [valuePercent, isDragging, isHovered]);\n\n  const handleValueChange = useCallback(\n    (values: number[]) => {\n      if (values[0] !== undefined) {\n        onChange(values[0]);\n      }\n    },\n    [onChange],\n  );\n\n  return (\n    <div className=\"py-2\">\n      <SliderPrimitive.Root\n        id={id}\n        className={cn(\n          \"group/slider relative flex w-full touch-none items-center select-none\",\n          \"isolate h-12\",\n          isDragging\n            ? \"[&>span]:transition-[left,transform] [&>span]:duration-45 [&>span]:ease-linear\"\n            : \"[&>span]:transition-[left,transform] [&>span]:duration-90 [&>span]:ease-[cubic-bezier(0.22,1,0.36,1)]\",\n          \"[&>span]:will-change-[left,transform]\",\n          \"motion-reduce:[&>span]:transition-none\",\n          disabled && \"pointer-events-none opacity-50\",\n        )}\n        value={[value]}\n        onValueChange={handleValueChange}\n        onPointerDown={() => setIsDragging(true)}\n        onPointerUp={() => setIsDragging(false)}\n        onPointerEnter={() => setIsHovered(true)}\n        onPointerLeave={() => setIsHovered(false)}\n        min={min}\n        max={max}\n        step={step}\n        disabled={disabled}\n        aria-valuetext={getAriaValueText(value, min, max, unit)}\n      >\n        <SliderPrimitive.Track\n          ref={trackRef}\n          className={cn(\n            \"squircle relative h-12 w-full grow overflow-hidden rounded-sm\",\n            \"ring-border ring-1 ring-inset\",\n            \"dark:ring-white/10\",\n            resolvedTrackClassName ?? \"bg-muted\",\n          )}\n        >\n          <div\n            className={cn(\n              \"absolute inset-0 will-change-[clip-path]\",\n              isDragging\n                ? \"transition-[clip-path] duration-45 ease-linear\"\n                : \"transition-[clip-path] duration-90 ease-[cubic-bezier(0.22,1,0.36,1)]\",\n              \"motion-reduce:transition-none\",\n              resolvedFillClassName ?? \"bg-primary/30 dark:bg-primary/40\",\n            )}\n            style={{\n              maskImage: fillMaskImage,\n              WebkitMaskImage: fillMaskImage,\n              clipPath: fillClipPath,\n            }}\n          />\n\n          {ticks.map((tick, i) => {\n            const isEdge =\n              !tick.isSubtick && (tick.percent === 0 || tick.percent === 100);\n            return (\n              <span\n                key={i}\n                className={cn(\n                  \"pointer-events-none absolute bottom-px w-px\",\n                  tick.isSubtick ? \"h-1.5\" : \"h-2\",\n                  isEdge\n                    ? \"bg-transparent\"\n                    : tick.isSubtick\n                      ? \"bg-foreground/8 dark:bg-white/5\"\n                      : tick.isCenter\n                        ? \"bg-foreground/30 dark:bg-white/25\"\n                        : \"bg-foreground/15 dark:bg-white/8\",\n                )}\n                style={{\n                  left: toInsetPosition(tick.percent),\n                  transform: \"translateX(-50%)\",\n                }}\n              />\n            );\n          })}\n        </SliderPrimitive.Track>\n\n        {/* Metallic reflection overlay - follows handle, brightness scales with interaction */}\n        <div\n          className={cn(\n            \"squircle pointer-events-none absolute inset-0 rounded-sm\",\n            isDragging\n              ? \"transition-[opacity,background] duration-45 ease-linear\"\n              : \"transition-[opacity,background] duration-90 ease-[cubic-bezier(0.22,1,0.36,1)]\",\n            \"motion-reduce:transition-none\",\n          )}\n          style={{\n            ...reflectionStyle,\n            opacity: reflectionOpacity,\n            filter: \"blur(1px)\",\n            mixBlendMode: \"overlay\",\n          }}\n        />\n\n        <SliderPrimitive.Thumb\n          className={cn(\n            \"group/thumb z-0 block w-3 shrink-0 cursor-grab rounded-sm\",\n            \"relative bg-transparent outline-none\",\n            \"transition-[height,opacity] duration-150 ease-[var(--cubic-ease-in-out)]\",\n            \"focus-visible:outline-ring focus-visible:outline-2 focus-visible:outline-offset-1\",\n            \"active:cursor-grabbing\",\n            \"disabled:pointer-events-none disabled:opacity-50\",\n            // Height morphs: rest (track height) → hover → active\n            isDragging ? \"h-[56px]\" : isHovered ? \"h-[54px]\" : \"h-12\",\n          )}\n        >\n          {(() => {\n            // Calculate morph state\n            const isActive = isHovered || isDragging;\n\n            // Indicator stays centered on the real thumb while CSS transitions\n            // smooth thumb wrapper and fill movement together.\n            const fillEdgeOffset = 0;\n\n            // Hide rest-state indicator at edges (0% or 100%) - the reflection gradient handles this\n            const edgeThreshold = 3;\n            const atEdge =\n              valuePercent <= edgeThreshold ||\n              valuePercent >= 100 - edgeThreshold;\n            const restOpacity = atEdge ? 0 : 0.25;\n\n            // Asymmetric segment heights: gap is shifted up to match raised text position\n            // Top segment is shorter, bottom segment is taller\n            const topHeight =\n              isActive && gap > 0\n                ? `calc(50% - ${gap / 2 + TEXT_VERTICAL_OFFSET}px)`\n                : \"50%\";\n            const bottomHeight =\n              isActive && gap > 0\n                ? `calc(50% - ${gap / 2 - TEXT_VERTICAL_OFFSET}px)`\n                : \"50%\";\n\n            return (\n              <>\n                <span\n                  className={cn(\n                    \"absolute top-0 left-1/2\",\n                    \"transition-all duration-100 ease-[var(--cubic-ease-in-out)]\",\n                    isActive\n                      ? gap > 0\n                        ? \"rounded-full\"\n                        : \"rounded-t-full\"\n                      : \"rounded-t-sm\",\n                    isDragging ? \"w-2\" : isActive ? \"w-1.5\" : \"w-px\",\n                    resolvedHandleClassName ?? \"bg-primary\",\n                  )}\n                  style={{\n                    transform: `translateX(calc(-50% + ${fillEdgeOffset}px))`,\n                    height: topHeight,\n                    opacity: isActive ? 1 : restOpacity,\n                  }}\n                />\n                <span\n                  className={cn(\n                    \"absolute bottom-0 left-1/2\",\n                    \"transition-all duration-100 ease-[var(--cubic-ease-in-out)]\",\n                    isActive\n                      ? gap > 0\n                        ? \"rounded-full\"\n                        : \"rounded-b-full\"\n                      : \"rounded-b-sm\",\n                    isDragging ? \"w-2\" : isActive ? \"w-1.5\" : \"w-px\",\n                    resolvedHandleClassName ?? \"bg-primary\",\n                  )}\n                  style={{\n                    transform: `translateX(calc(-50% + ${fillEdgeOffset}px))`,\n                    height: bottomHeight,\n                    opacity: isActive ? 1 : restOpacity,\n                  }}\n                />\n              </>\n            );\n          })()}\n        </SliderPrimitive.Thumb>\n\n        <div\n          className=\"pointer-events-none absolute inset-x-3 top-1/2 z-10 flex items-center justify-between\"\n          style={{\n            transform: `translateY(calc(-50% - ${TEXT_VERTICAL_OFFSET}px))`,\n          }}\n        >\n          <span\n            ref={labelRef}\n            className=\"text-primary -mt-px rounded-full px-2 py-px text-sm font-normal tracking-wide\"\n          >\n            {label}\n          </span>\n          <span\n            ref={valueRef}\n            className=\"text-foreground -mt-px -mb-0.5 flex h-6 items-center rounded-full px-2 font-mono text-xs tabular-nums\"\n          >\n            {formatSignedValue(value, min, max, precision, unit)}\n          </span>\n        </div>\n      </SliderPrimitive.Root>\n    </div>\n  );\n}\n\nexport function ParameterSlider({\n  id,\n  sliders,\n  values: controlledValues,\n  onChange,\n  actions,\n  onAction,\n  onBeforeAction,\n  className,\n  trackClassName,\n  fillClassName,\n  handleClassName,\n}: ParameterSliderProps) {\n  const slidersSignature = useMemo(\n    () => createSliderSignature(sliders),\n    [sliders],\n  );\n  const sliderSnapshot = useMemo(\n    () => createSliderValueSnapshot(sliders),\n    [sliders],\n  );\n  const {\n    value: currentValues,\n    isControlled,\n    setValue,\n    setUncontrolledValue,\n  } = useControllableState<SliderValue[]>({\n    value: controlledValues,\n    defaultValue: sliderSnapshot,\n    onChange,\n  });\n\n  useSignatureReset(slidersSignature, () => {\n    if (!isControlled) {\n      setUncontrolledValue(sliderSnapshot);\n    }\n  });\n\n  const valueMap = useMemo(() => {\n    const map = new Map<string, number>();\n    for (const v of currentValues) {\n      map.set(v.id, v.value);\n    }\n    return map;\n  }, [currentValues]);\n\n  const updateValue = useCallback(\n    (sliderId: string, newValue: number) => {\n      setValue((prev) =>\n        prev.map((v) => (v.id === sliderId ? { ...v, value: newValue } : v)),\n      );\n    },\n    [setValue],\n  );\n\n  const handleReset = useCallback(() => {\n    setValue(sliderSnapshot);\n  }, [setValue, sliderSnapshot]);\n\n  const handleAction = useCallback(\n    async (actionId: string) => {\n      let nextValues = currentValues;\n      if (actionId === \"reset\") {\n        handleReset();\n        nextValues = sliderSnapshot;\n      }\n\n      await onAction?.(actionId, nextValues);\n    },\n    [currentValues, handleReset, onAction, sliderSnapshot],\n  );\n\n  const normalizedActions = useMemo(() => {\n    const normalized = normalizeActionsConfig(actions);\n    if (normalized) return normalized;\n    return {\n      items: [\n        { id: \"reset\", label: \"Reset\", variant: \"ghost\" as const },\n        { id: \"apply\", label: \"Apply\", variant: \"default\" as const },\n      ],\n      align: \"right\" as const,\n    };\n  }, [actions]);\n\n  return (\n    <article\n      className={cn(\n        \"@container/parameter-slider isolate flex w-full max-w-md min-w-80 flex-col gap-3\",\n        \"text-foreground\",\n        className,\n      )}\n      data-slot=\"parameter-slider\"\n      data-tool-ui-id={id}\n    >\n      <div\n        className={cn(\n          \"bg-card flex w-full flex-col overflow-hidden rounded-2xl border px-5 py-3 shadow-xs\",\n        )}\n      >\n        {sliders.map((slider) => (\n          <SliderRow\n            key={slider.id}\n            config={slider}\n            value={valueMap.get(slider.id) ?? slider.value}\n            onChange={(v) => updateValue(slider.id, v)}\n            trackClassName={trackClassName}\n            fillClassName={fillClassName}\n            handleClassName={handleClassName}\n          />\n        ))}\n      </div>\n\n      <div className=\"@container/actions\">\n        <ActionButtons\n          actions={normalizedActions.items}\n          align={normalizedActions.align}\n          confirmTimeout={normalizedActions.confirmTimeout}\n          onAction={handleAction}\n          onBeforeAction={\n            onBeforeAction\n              ? (actionId) => onBeforeAction(actionId, currentValues)\n              : undefined\n          }\n        />\n      </div>\n    </article>\n  );\n}\n"
    },
    {
      "path": "components/tool-ui/parameter-slider/README.md",
      "type": "registry:file",
      "target": "components/tool-ui/parameter-slider/README.md",
      "content": "# Parameter Slider\n\nImplementation for the \"parameter-slider\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/parameter-slider/index.tsx\n- serializable schema + parse helpers: components/tool-ui/parameter-slider/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/parameter-slider/content.mdx\n- Preset payload: lib/presets/parameter-slider.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
    },
    {
      "path": "components/tool-ui/parameter-slider/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/parameter-slider/schema.ts",
      "content": "import { z } from \"zod\";\nimport { type ActionsProp } from \"../shared/actions-config\";\nimport type { EmbeddedActionsProps } from \"../shared/embedded-actions\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  SerializableActionSchema,\n  SerializableActionsConfigSchema,\n  ToolUIIdSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nexport const SliderConfigSchema = z\n  .object({\n    id: z.string().min(1),\n    label: z.string().min(1),\n    min: z.number().finite(),\n    max: z.number().finite(),\n    step: z.number().finite().positive().optional(),\n    value: z.number().finite(),\n    unit: z.string().optional(),\n    precision: z.number().int().min(0).optional(),\n    disabled: z.boolean().optional(),\n    trackClassName: z.string().optional(),\n    fillClassName: z.string().optional(),\n    handleClassName: z.string().optional(),\n  })\n  .superRefine((slider, ctx) => {\n    if (slider.max <= slider.min) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        path: [\"max\"],\n        message: \"max must be greater than min\",\n      });\n    }\n\n    if (slider.value < slider.min || slider.value > slider.max) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        path: [\"value\"],\n        message: \"value must be between min and max\",\n      });\n    }\n  });\n\nexport type SliderConfig = z.infer<typeof SliderConfigSchema>;\n\nexport const SerializableParameterSliderSchema = z\n  .object({\n    id: ToolUIIdSchema,\n    role: ToolUIRoleSchema.optional(),\n    sliders: z.array(SliderConfigSchema).min(1),\n    actions: z\n      .union([\n        z.array(SerializableActionSchema),\n        SerializableActionsConfigSchema,\n      ])\n      .optional(),\n  })\n  .strict()\n  .superRefine((payload, ctx) => {\n    const seenIds = new Map<string, number>();\n\n    payload.sliders.forEach((slider, index) => {\n      const firstSeenAt = seenIds.get(slider.id);\n      if (firstSeenAt !== undefined) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          path: [\"sliders\", index, \"id\"],\n          message: `duplicate slider id '${slider.id}' (first seen at index ${firstSeenAt})`,\n        });\n        return;\n      }\n      seenIds.set(slider.id, index);\n    });\n  });\n\nexport type SerializableParameterSlider = z.infer<\n  typeof SerializableParameterSliderSchema\n>;\n\nconst SerializableParameterSliderSchemaContract = defineToolUiContract(\n  \"ParameterSlider\",\n  SerializableParameterSliderSchema,\n);\n\nexport const parseSerializableParameterSlider: (\n  input: unknown,\n) => SerializableParameterSlider =\n  SerializableParameterSliderSchemaContract.parse;\n\nexport const safeParseSerializableParameterSlider: (\n  input: unknown,\n) => SerializableParameterSlider | null =\n  SerializableParameterSliderSchemaContract.safeParse;\n\nexport interface SliderValue {\n  id: string;\n  value: number;\n}\n\nexport interface ParameterSliderProps extends Omit<\n  SerializableParameterSlider,\n  \"actions\"\n> {\n  className?: string;\n  values?: SliderValue[];\n  onChange?: (values: SliderValue[]) => void;\n  actions?: ActionsProp;\n  onAction?: EmbeddedActionsProps<SliderValue[]>[\"onAction\"];\n  onBeforeAction?: EmbeddedActionsProps<SliderValue[]>[\"onBeforeAction\"];\n  trackClassName?: string;\n  fillClassName?: string;\n  handleClassName?: string;\n}\n"
    },
    {
      "path": "components/tool-ui/shared/_adapter.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/shared/_adapter.tsx",
      "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn     → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button → shadcn/ui Button\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\n"
    },
    {
      "path": "components/tool-ui/shared/action-buttons.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/shared/action-buttons.tsx",
      "content": "\"use client\";\n\nimport type { Action } from \"./schema\";\nimport { cn, Button } from \"./_adapter\";\nimport { useActionButtons } from \"./use-action-buttons\";\n\nexport interface ActionButtonsProps {\n  actions: Action[];\n  onAction: (actionId: string) => void | Promise<void>;\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\n  confirmTimeout?: number;\n  align?: \"left\" | \"center\" | \"right\";\n  className?: string;\n}\n\nexport function ActionButtons({\n  actions,\n  onAction,\n  onBeforeAction,\n  confirmTimeout = 3000,\n  align = \"right\",\n  className,\n}: ActionButtonsProps) {\n  const { actions: resolvedActions, runAction } = useActionButtons({\n    actions,\n    onAction,\n    onBeforeAction,\n    confirmTimeout,\n  });\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-col gap-3\",\n        \"@sm/actions:flex-row @sm/actions:flex-wrap @sm/actions:items-center @sm/actions:gap-2\",\n        align === \"left\" && \"@sm/actions:justify-start\",\n        align === \"center\" && \"@sm/actions:justify-center\",\n        align === \"right\" && \"@sm/actions:justify-end\",\n        className,\n      )}\n    >\n      {resolvedActions.map((action) => {\n        const label = action.currentLabel;\n        const variant = action.variant || \"default\";\n\n        return (\n          <Button\n            key={action.id}\n            variant={variant}\n            onClick={() => runAction(action.id)}\n            disabled={action.isDisabled}\n            className={cn(\n              \"rounded-full px-4!\",\n              \"justify-center\",\n              \"min-h-11 w-full text-base\",\n              \"@sm/actions:min-h-0 @sm/actions:w-auto @sm/actions:px-3 @sm/actions:py-2 @sm/actions:text-sm\",\n              action.isConfirming &&\n                \"ring-destructive ring-2 ring-offset-2 motion-safe:animate-pulse\",\n            )}\n            aria-label={\n              action.shortcut ? `${label} (${action.shortcut})` : label\n            }\n          >\n            {action.isLoading && (\n              <svg\n                className=\"mr-2 h-4 w-4 motion-safe:animate-spin\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n              >\n                <circle\n                  className=\"opacity-25\"\n                  cx=\"12\"\n                  cy=\"12\"\n                  r=\"10\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"4\"\n                />\n                <path\n                  className=\"opacity-75\"\n                  fill=\"currentColor\"\n                  d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n                />\n              </svg>\n            )}\n            {action.icon && !action.isLoading && (\n              <span className=\"mr-2\">{action.icon}</span>\n            )}\n            {label}\n            {action.shortcut && !action.isLoading && (\n              <kbd className=\"border-border bg-muted ml-2.5 hidden rounded-lg border px-2 py-0.5 font-mono text-xs font-medium sm:inline-block\">\n                {action.shortcut}\n              </kbd>\n            )}\n          </Button>\n        );\n      })}\n    </div>\n  );\n}\n"
    },
    {
      "path": "components/tool-ui/shared/actions-config.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/actions-config.ts",
      "content": "import type { Action, ActionsConfig } from \"./schema\";\n\nexport type ActionsProp = ActionsConfig | Action[];\n\nconst NEGATORY_ACTION_IDS = new Set([\n  \"cancel\",\n  \"dismiss\",\n  \"skip\",\n  \"no\",\n  \"reset\",\n  \"close\",\n  \"decline\",\n  \"reject\",\n  \"back\",\n  \"later\",\n  \"not-now\",\n  \"maybe-later\",\n]);\n\nfunction inferVariant(action: Action): Action {\n  if (action.variant) return action;\n  if (NEGATORY_ACTION_IDS.has(action.id)) {\n    return { ...action, variant: \"ghost\" };\n  }\n  return action;\n}\n\nexport function normalizeActionsConfig(\n  actions?: ActionsProp,\n): ActionsConfig | null {\n  if (!actions) return null;\n\n  const rawItems = Array.isArray(actions) ? actions : (actions.items ?? []);\n\n  if (rawItems.length === 0) {\n    return null;\n  }\n\n  const items = rawItems.map(inferVariant);\n\n  return Array.isArray(actions)\n    ? { items }\n    : {\n        items,\n        align: actions.align,\n        confirmTimeout: actions.confirmTimeout,\n      };\n}\n"
    },
    {
      "path": "components/tool-ui/shared/contract.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/contract.ts",
      "content": "import { z } from \"zod\";\nimport { parseWithSchema, safeParseWithSchema } from \"./parse\";\n\nexport interface ToolUiContract<T> {\n  schema: z.ZodType<T>;\n  parse: (input: unknown) => T;\n  safeParse: (input: unknown) => T | null;\n}\n\nexport function defineToolUiContract<T>(\n  componentName: string,\n  schema: z.ZodType<T>,\n): ToolUiContract<T> {\n  return {\n    schema,\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\n  };\n}\n"
    },
    {
      "path": "components/tool-ui/shared/embedded-actions.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/embedded-actions.ts",
      "content": "import type { ActionsProp } from \"./actions-config\";\n\nexport type EmbeddedActionHandler<TState> = (\n  actionId: string,\n  state: TState,\n) => void | Promise<void>;\n\nexport type EmbeddedBeforeActionHandler<TState> = (\n  actionId: string,\n  state: TState,\n) => boolean | Promise<boolean>;\n\nexport interface EmbeddedActionsProps<TState> {\n  actions?: ActionsProp;\n  onAction?: EmbeddedActionHandler<TState>;\n  onBeforeAction?: EmbeddedBeforeActionHandler<TState>;\n}\n"
    },
    {
      "path": "components/tool-ui/shared/parse.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/parse.ts",
      "content": "import { z } from \"zod\";\n\nfunction formatZodPath(path: Array<string | number | symbol>): string {\n  if (path.length === 0) return \"root\";\n  return path\n    .map((segment) =>\n      typeof segment === \"number\" ? `[${segment}]` : String(segment),\n    )\n    .join(\".\");\n}\n\n/**\n * Format Zod errors into a compact `path: message` string.\n */\nexport function formatZodError(error: z.ZodError): string {\n  const parts = error.issues.map((issue) => {\n    const path = formatZodPath(issue.path);\n    return `${path}: ${issue.message}`;\n  });\n\n  return Array.from(new Set(parts)).join(\"; \");\n}\n\n/**\n * Parse unknown input and throw a readable error.\n */\nexport function parseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n  name: string,\n): T {\n  const res = schema.safeParse(input);\n  if (!res.success) {\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\n  }\n  return res.data;\n}\n\n/**\n * Parse unknown input, returning `null` instead of throwing on failure.\n *\n * Use this in assistant-ui `render` functions where `args` stream in\n * incrementally and may be incomplete until the tool call finishes.\n */\nexport function safeParseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n): T | null {\n  const res = schema.safeParse(input);\n  return res.success ? res.data : null;\n}\n"
    },
    {
      "path": "components/tool-ui/shared/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/schema.ts",
      "content": "import { z } from \"zod\";\nimport type { ReactNode } from \"react\";\n\n/**\n * Tool UI conventions:\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\n * - Schema: `SerializableXSchema`\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\n * - Root attrs: `data-tool-ui-id` + `data-slot`\n */\n\n/**\n * Schema for tool UI identity.\n *\n * Every tool UI should have a unique identifier that:\n * - Is stable across re-renders\n * - Is meaningful (not auto-generated)\n * - Is unique within the conversation\n *\n * Format recommendation: `{component-type}-{semantic-identifier}`\n * Examples: \"data-table-expenses-q3\", \"option-list-deploy-target\"\n */\nexport const ToolUIIdSchema = z.string().min(1);\n\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\n\n/**\n * Primary role of a Tool UI surface in a chat context.\n */\nexport const ToolUIRoleSchema = z.enum([\n  \"information\",\n  \"decision\",\n  \"control\",\n  \"state\",\n  \"composite\",\n]);\n\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\n\nexport const ToolUIReceiptOutcomeSchema = z.enum([\n  \"success\",\n  \"partial\",\n  \"failed\",\n  \"cancelled\",\n]);\n\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\n\n/**\n * Optional receipt metadata: a durable summary of an outcome.\n */\nexport const ToolUIReceiptSchema = z.object({\n  outcome: ToolUIReceiptOutcomeSchema,\n  summary: z.string().min(1),\n  identifiers: z.record(z.string(), z.string()).optional(),\n  at: z.string().datetime(),\n});\n\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\n\n/**\n * Base schema for Tool UI payloads (id + optional role/receipt).\n */\nexport const ToolUISurfaceSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n});\n\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\n\nexport const ActionSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  /**\n   * Canonical narration the assistant can use after this action is taken.\n   *\n   * Example: \"I exported the table as CSV.\" / \"I opened the link in a new tab.\"\n   */\n  sentence: z.string().optional(),\n  confirmLabel: z.string().optional(),\n  variant: z\n    .enum([\"default\", \"destructive\", \"secondary\", \"ghost\", \"outline\"])\n    .optional(),\n  icon: z.custom<ReactNode>().optional(),\n  loading: z.boolean().optional(),\n  disabled: z.boolean().optional(),\n  shortcut: z.string().optional(),\n});\n\nexport type Action = z.infer<typeof ActionSchema>;\nexport type LocalAction = Action;\nexport type DecisionAction = Action;\n\nexport const DecisionResultSchema = z.object({\n  kind: z.literal(\"decision\"),\n  version: z.literal(1),\n  decisionId: z.string().min(1),\n  actionId: z.string().min(1),\n  actionLabel: z.string().min(1),\n  at: z.string().datetime(),\n  payload: z.record(z.string(), z.unknown()).optional(),\n});\n\nexport type DecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n> = Omit<z.infer<typeof DecisionResultSchema>, \"payload\"> & {\n  payload?: TPayload;\n};\n\nexport function createDecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n>(args: {\n  decisionId: string;\n  action: { id: string; label: string };\n  payload?: TPayload;\n}): DecisionResult<TPayload> {\n  return {\n    kind: \"decision\",\n    version: 1,\n    decisionId: args.decisionId,\n    actionId: args.action.id,\n    actionLabel: args.action.label,\n    at: new Date().toISOString(),\n    payload: args.payload,\n  };\n}\n\nexport const ActionButtonsPropsSchema = z.object({\n  actions: z.array(ActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n  className: z.string().optional(),\n});\n\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\n  actions: z.array(SerializableActionSchema),\n}).omit({ className: true });\n\nexport interface ActionsConfig {\n  items: Action[];\n  align?: \"left\" | \"center\" | \"right\";\n  confirmTimeout?: number;\n}\n\nexport const SerializableActionsConfigSchema = z.object({\n  items: z.array(SerializableActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n});\n\nexport type SerializableActionsConfig = z.infer<\n  typeof SerializableActionsConfigSchema\n>;\n\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\n"
    },
    {
      "path": "components/tool-ui/shared/use-action-buttons.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/shared/use-action-buttons.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { Action } from \"./schema\";\n\nexport type UseActionButtonsOptions = {\n  actions: Action[];\n  onAction: (actionId: string) => void | Promise<void>;\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\n  confirmTimeout?: number;\n};\n\nexport type UseActionButtonsResult = {\n  actions: Array<\n    Action & {\n      currentLabel: string;\n      isConfirming: boolean;\n      isExecuting: boolean;\n      isDisabled: boolean;\n      isLoading: boolean;\n    }\n  >;\n  runAction: (actionId: string) => Promise<void>;\n  confirmingActionId: string | null;\n  executingActionId: string | null;\n};\n\ntype ActionExecutionLock = {\n  tryAcquire: () => boolean;\n  release: () => void;\n};\n\nexport function createActionExecutionLock(): ActionExecutionLock {\n  let locked = false;\n\n  return {\n    tryAcquire: () => {\n      if (locked) return false;\n      locked = true;\n      return true;\n    },\n    release: () => {\n      locked = false;\n    },\n  };\n}\n\nexport function useActionButtons(\n  options: UseActionButtonsOptions,\n): UseActionButtonsResult {\n  const { actions, onAction, onBeforeAction, confirmTimeout = 3000 } = options;\n\n  const [confirmingActionId, setConfirmingActionId] = useState<string | null>(\n    null,\n  );\n  const [executingActionId, setExecutingActionId] = useState<string | null>(\n    null,\n  );\n  const executionLockRef = useRef<ActionExecutionLock>(\n    createActionExecutionLock(),\n  );\n\n  useEffect(() => {\n    if (!confirmingActionId) return;\n    const id = setTimeout(() => setConfirmingActionId(null), confirmTimeout);\n    return () => clearTimeout(id);\n  }, [confirmingActionId, confirmTimeout]);\n\n  useEffect(() => {\n    if (!confirmingActionId) return;\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        setConfirmingActionId(null);\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [confirmingActionId]);\n\n  const runAction = useCallback(\n    async (actionId: string) => {\n      const action = actions.find((a) => a.id === actionId);\n      if (!action) return;\n\n      const isAnyActionExecuting = executingActionId !== null;\n      if (action.disabled || action.loading || isAnyActionExecuting) {\n        return;\n      }\n\n      if (action.confirmLabel && confirmingActionId !== action.id) {\n        setConfirmingActionId(action.id);\n        return;\n      }\n\n      if (!executionLockRef.current.tryAcquire()) {\n        return;\n      }\n\n      if (onBeforeAction) {\n        const shouldProceed = await onBeforeAction(action.id);\n        if (!shouldProceed) {\n          setConfirmingActionId(null);\n          executionLockRef.current.release();\n          return;\n        }\n      }\n\n      try {\n        setExecutingActionId(action.id);\n        await onAction(action.id);\n      } finally {\n        executionLockRef.current.release();\n        setExecutingActionId(null);\n        setConfirmingActionId(null);\n      }\n    },\n    [actions, confirmingActionId, executingActionId, onAction, onBeforeAction],\n  );\n\n  const resolvedActions = useMemo(\n    () =>\n      actions.map((action) => {\n        const isConfirming = confirmingActionId === action.id;\n        const isThisActionExecuting = executingActionId === action.id;\n        const isLoading = action.loading || isThisActionExecuting;\n        const isDisabled =\n          action.disabled ||\n          (executingActionId !== null && !isThisActionExecuting);\n        const currentLabel =\n          isConfirming && action.confirmLabel\n            ? action.confirmLabel\n            : action.label;\n\n        return {\n          ...action,\n          currentLabel,\n          isConfirming,\n          isExecuting: isThisActionExecuting,\n          isDisabled,\n          isLoading,\n        };\n      }),\n    [actions, confirmingActionId, executingActionId],\n  );\n\n  return {\n    actions: resolvedActions,\n    runAction,\n    confirmingActionId,\n    executingActionId,\n  };\n}\n"
    },
    {
      "path": "components/tool-ui/shared/use-controllable-state.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/use-controllable-state.ts",
      "content": "\"use client\";\n\nimport { useCallback, useMemo, useRef, useState } from \"react\";\n\nexport type UseControllableStateOptions<T> = {\n  value?: T;\n  defaultValue: T;\n  onChange?: (next: T) => void;\n};\n\nexport function useControllableState<T>({\n  value,\n  defaultValue,\n  onChange,\n}: UseControllableStateOptions<T>) {\n  const [uncontrolled, setUncontrolled] = useState<T>(defaultValue);\n  const isControlled = value !== undefined;\n\n  const currentValue = useMemo(\n    () => (isControlled ? (value as T) : uncontrolled),\n    [isControlled, value, uncontrolled],\n  );\n  const currentValueRef = useRef(currentValue);\n  currentValueRef.current = currentValue;\n\n  const setValue = useCallback(\n    (next: T | ((prev: T) => T)) => {\n      const resolved =\n        typeof next === \"function\"\n          ? (next as (prev: T) => T)(currentValueRef.current)\n          : next;\n\n      currentValueRef.current = resolved;\n      if (!isControlled) {\n        setUncontrolled(resolved);\n      }\n\n      onChange?.(resolved);\n      return resolved;\n    },\n    [isControlled, onChange],\n  );\n\n  const setUncontrolledValue = useCallback((next: T) => {\n    setUncontrolled(next);\n  }, []);\n\n  return {\n    value: currentValue,\n    isControlled,\n    setValue,\n    setUncontrolledValue,\n  };\n}\n"
    },
    {
      "path": "components/tool-ui/shared/use-signature-reset.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/use-signature-reset.ts",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport function useSignatureReset(\n  signature: string,\n  onSignatureChange: () => void,\n) {\n  const previousSignature = useRef(signature);\n\n  useEffect(() => {\n    if (previousSignature.current === signature) return;\n    previousSignature.current = signature;\n    onSignatureChange();\n  }, [signature, onSignatureChange]);\n}\n"
    }
  ]
}
