{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mac-slider",
  "type": "registry:ui",
  "title": "Mac Slider",
  "description": "Liquid glass slider with squircle normal map refraction, specular rim sheen, and tactile spring physics.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/mac-slider.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef, useId, useCallback, useState } from \"react\";\nimport {\n  motion,\n  useMotionValue,\n  useMotionValueEvent,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n  animate,\n  type MotionValue,\n} from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type MacSliderColor =\n  | \"blue\"\n  | \"emerald\"\n  | \"violet\"\n  | \"amber\"\n  | \"rose\"\n  | \"cyan\"\n  | \"monochrome\";\n\nexport type MacSliderSize = \"sm\" | \"md\" | \"lg\";\nexport type MacSliderMaterial = \"liquid\" | \"frosted\" | \"clear\";\n\nexport interface FilterProps {\n  id?: string;\n  blur?: MotionValue<number> | number;\n  scaleRatio?: MotionValue<number> | number;\n  specularOpacity?: MotionValue<number> | number;\n  specularSaturation?: MotionValue<number> | number;\n  width?: number;\n  height?: number;\n  radius?: number;\n  bezelWidth?: number;\n}\n\nconst COLOR_THEMES: Record<\n  MacSliderColor,\n  { fill: string; dot: string; label: string; glow: string }\n> = {\n  blue: {\n    fill: \"#007AFF\",\n    dot: \"bg-blue-500\",\n    label: \"Blue\",\n    glow: \"rgba(0, 122, 255, 0.35)\",\n  },\n  emerald: {\n    fill: \"#10B981\",\n    dot: \"bg-emerald-400\",\n    label: \"Emerald\",\n    glow: \"rgba(16, 185, 129, 0.35)\",\n  },\n  violet: {\n    fill: \"#8B5CF6\",\n    dot: \"bg-violet-400\",\n    label: \"Violet\",\n    glow: \"rgba(139, 92, 246, 0.35)\",\n  },\n  amber: {\n    fill: \"#F59E0B\",\n    dot: \"bg-amber-400\",\n    label: \"Amber\",\n    glow: \"rgba(245, 158, 11, 0.35)\",\n  },\n  rose: {\n    fill: \"#F43F5E\",\n    dot: \"bg-rose-400\",\n    label: \"Rose\",\n    glow: \"rgba(244, 63, 94, 0.35)\",\n  },\n  cyan: {\n    fill: \"#06B6D4\",\n    dot: \"bg-cyan-400\",\n    label: \"Cyan\",\n    glow: \"rgba(6, 182, 212, 0.35)\",\n  },\n  monochrome: {\n    fill: \"#F4F4F5\",\n    dot: \"bg-zinc-200\",\n    label: \"Graphite\",\n    glow: \"rgba(255, 255, 255, 0.25)\",\n  },\n};\n\nconst SIZE_CONFIGS: Record<\n  MacSliderSize,\n  {\n    sliderWidth: number;\n    sliderHeight: number;\n    thumbWidth: number;\n    thumbHeight: number;\n    thumbRadius: number;\n    bezelWidth: number;\n  }\n> = {\n  sm: {\n    sliderWidth: 260,\n    sliderHeight: 10,\n    thumbWidth: 72,\n    thumbHeight: 48,\n    thumbRadius: 24,\n    bezelWidth: 12,\n  },\n  md: {\n    sliderWidth: 330,\n    sliderHeight: 14,\n    thumbWidth: 90,\n    thumbHeight: 60,\n    thumbRadius: 30,\n    bezelWidth: 16,\n  },\n  lg: {\n    sliderWidth: 400,\n    sliderHeight: 18,\n    thumbWidth: 108,\n    thumbHeight: 72,\n    thumbRadius: 36,\n    bezelWidth: 20,\n  },\n};\n\ntype SpringConfig = Parameters<typeof useSpring>[1];\n\nfunction useReducibleSpring(\n  source: MotionValue<number>,\n  config: SpringConfig,\n  instant: boolean,\n): MotionValue<number> {\n  const spring = useSpring(source, config);\n  return instant ? source : spring;\n}\n\nfunction isMotionValue(val: unknown): val is MotionValue<number> {\n  return typeof val === \"object\" && val !== null && \"get\" in val && \"on\" in val;\n}\n\nfunction getNumericValue(\n  val?: MotionValue<number> | number,\n  fallback = 0,\n): number {\n  if (val === undefined) return fallback;\n  if (isMotionValue(val)) return Number(val.get()) || fallback;\n  return Number(val) || fallback;\n}\n\nexport const Filter: React.FC<FilterProps> = ({\n  id = \"thumb-filter-slider\",\n  blur,\n  scaleRatio,\n  specularOpacity,\n  specularSaturation,\n  width = 90,\n  height = 60,\n  radius = 30,\n  bezelWidth = 16,\n}) => {\n  const feImageRef = useRef<SVGFEImageElement>(null);\n  const blurRef = useRef<SVGFEGaussianBlurElement>(null);\n  const dispRef = useRef<SVGFEDisplacementMapElement>(null);\n  const specRef = useRef<SVGFESpecularLightingElement>(null);\n  const satRef = useRef<SVGFEColorMatrixElement>(null);\n\n  useEffect(() => {\n    if (typeof document === \"undefined\") return;\n\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = width;\n    canvas.height = height;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const imgData = ctx.createImageData(width, height);\n    const data = imgData.data;\n    let pos = 0;\n\n    for (let y = 0; y < height; y++) {\n      for (let x = 0; x < width; x++) {\n        const segX = Math.max(radius, Math.min(width - radius, x));\n        const segY = height / 2;\n        const dx = x - segX;\n        const dy = y - segY;\n        const dist = Math.sqrt(dx * dx + dy * dy);\n        const distEdge = radius - dist;\n\n        if (distEdge <= 0 || distEdge >= bezelWidth) {\n          data[pos++] = 128;\n          data[pos++] = 128;\n          data[pos++] = 255;\n          data[pos++] = 255;\n        } else {\n          const u = distEdge / bezelWidth;\n          const slope = Math.cos((u * Math.PI) / 2);\n          const dirX = dist > 0.001 ? dx / dist : 0;\n          const dirY = dist > 0.001 ? dy / dist : 0;\n          const nx = -dirX * slope * 0.85;\n          const ny = -dirY * slope * 0.85;\n          const nz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny));\n\n          data[pos++] = Math.round(128 + 127 * nx);\n          data[pos++] = Math.round(128 + 127 * ny);\n          data[pos++] = Math.round(128 + 127 * nz);\n          data[pos++] = 255;\n        }\n      }\n    }\n\n    ctx.putImageData(imgData, 0, 0);\n    const dataUrl = canvas.toDataURL(\"image/png\");\n    feImageRef.current?.setAttribute(\"href\", dataUrl);\n  }, [width, height, radius, bezelWidth]);\n\n  useEffect(() => {\n    const unsubBlur = isMotionValue(blur)\n      ? blur.on(\"change\", (v) => {\n          blurRef.current?.setAttribute(\"stdDeviation\", String(v));\n        })\n      : undefined;\n\n    const unsubDisp = isMotionValue(scaleRatio)\n      ? scaleRatio.on(\"change\", (v) => {\n          dispRef.current?.setAttribute(\"scale\", String(v * 24));\n        })\n      : undefined;\n\n    const unsubSpec = isMotionValue(specularOpacity)\n      ? specularOpacity.on(\"change\", (v) => {\n          specRef.current?.setAttribute(\"specularConstant\", String(v * 1.5));\n        })\n      : undefined;\n\n    const unsubSat = isMotionValue(specularSaturation)\n      ? specularSaturation.on(\"change\", (v) => {\n          satRef.current?.setAttribute(\"values\", String(1 + v * 0.15));\n        })\n      : undefined;\n\n    const currentBlur = getNumericValue(blur, 0);\n    const currentScale = getNumericValue(scaleRatio, 0.4);\n    const currentSpec = getNumericValue(specularOpacity, 0.4);\n    const currentSat = getNumericValue(specularSaturation, 6);\n\n    blurRef.current?.setAttribute(\"stdDeviation\", String(currentBlur));\n    dispRef.current?.setAttribute(\"scale\", String(currentScale * 24));\n    specRef.current?.setAttribute(\n      \"specularConstant\",\n      String(currentSpec * 1.5),\n    );\n    satRef.current?.setAttribute(\"values\", String(1 + currentSat * 0.15));\n\n    return () => {\n      unsubBlur?.();\n      unsubDisp?.();\n      unsubSpec?.();\n      unsubSat?.();\n    };\n  }, [blur, scaleRatio, specularOpacity, specularSaturation]);\n\n  const initialBlur = getNumericValue(blur, 0);\n  const initialDisp = getNumericValue(scaleRatio, 0.4) * 24;\n  const initialSpec = getNumericValue(specularOpacity, 0.4) * 1.5;\n  const initialSat = 1 + getNumericValue(specularSaturation, 6) * 0.15;\n\n  return (\n    <svg\n      width=\"0\"\n      height=\"0\"\n      className=\"pointer-events-none absolute -z-10 opacity-0\"\n      aria-hidden=\"true\"\n    >\n      <defs>\n        <filter\n          id={id}\n          x=\"-20%\"\n          y=\"-20%\"\n          width=\"140%\"\n          height=\"140%\"\n          colorInterpolationFilters=\"sRGB\"\n        >\n          <feImage\n            ref={feImageRef}\n            result=\"normalMap\"\n            x=\"0\"\n            y=\"0\"\n            width={width}\n            height={height}\n            preserveAspectRatio=\"none\"\n          />\n          <feGaussianBlur\n            ref={blurRef}\n            in=\"SourceGraphic\"\n            stdDeviation={initialBlur}\n            result=\"blurred\"\n          />\n          <feDisplacementMap\n            ref={dispRef}\n            in=\"blurred\"\n            in2=\"normalMap\"\n            scale={initialDisp}\n            xChannelSelector=\"R\"\n            yChannelSelector=\"G\"\n            result=\"refracted\"\n          />\n          <feSpecularLighting\n            ref={specRef}\n            in=\"normalMap\"\n            surfaceScale=\"2\"\n            specularConstant={initialSpec}\n            specularExponent=\"24\"\n            result=\"specular\"\n            lightingColor=\"#ffffff\"\n          >\n            <feDistantLight azimuth=\"235\" elevation=\"55\" />\n          </feSpecularLighting>\n          <feColorMatrix\n            ref={satRef}\n            in=\"refracted\"\n            type=\"saturate\"\n            values={String(initialSat)}\n            result=\"saturated\"\n          />\n          <feBlend in=\"saturated\" in2=\"specular\" mode=\"screen\" />\n        </filter>\n      </defs>\n    </svg>\n  );\n};\n\nexport interface MacSliderProps {\n  min?: number;\n  max?: number;\n  step?: number;\n  defaultValue?: number;\n  value?: number;\n  onChange?: (value: number) => void;\n  color?: MacSliderColor;\n  size?: MacSliderSize;\n  material?: MacSliderMaterial;\n  forceActive?: boolean;\n  specularOpacity?: number;\n  specularSaturation?: number;\n  refractionLevel?: number;\n  blurLevel?: number;\n  className?: string;\n  disabled?: boolean;\n  label?: string;\n}\n\nexport const MacSlider: React.FC<MacSliderProps> = ({\n  min = 0,\n  max = 100,\n  step = 1,\n  defaultValue = 10,\n  value: controlledValue,\n  onChange,\n  color = \"blue\",\n  size = \"md\",\n  material = \"liquid\",\n  forceActive = false,\n  specularOpacity: specularOpacityProp = 0.4,\n  specularSaturation: specularSaturationProp = 6,\n  refractionLevel = 0.28,\n  blurLevel = 0,\n  className,\n  disabled = false,\n  label = \"Value\",\n}) => {\n  const generatedId = useId();\n  const filterId = `mac-filter-${generatedId.replace(/:/g, \"\")}`;\n  const reduceMotion = useReducedMotion() ?? false;\n\n  const currentTheme = COLOR_THEMES[color] ?? COLOR_THEMES.blue;\n  const currentSize = SIZE_CONFIGS[size] ?? SIZE_CONFIGS.md;\n  const {\n    sliderWidth,\n    sliderHeight,\n    thumbWidth,\n    thumbHeight,\n    thumbRadius,\n    bezelWidth,\n  } = currentSize;\n\n  const SCALE_REST = 0.6;\n  const SCALE_DRAG = 1;\n  const thumbWidthRest = thumbWidth * SCALE_REST;\n  const trackTravel = sliderWidth - thumbWidthRest;\n  const minX = -thumbWidthRest / 3;\n\n  const initialVal =\n    controlledValue !== undefined ? controlledValue : defaultValue;\n  const valueMotion = useMotionValue(initialVal);\n  const [ariaValue, setAriaValue] = useState(initialVal);\n  useMotionValueEvent(valueMotion, \"change\", setAriaValue);\n  const initialRatio = Math.max(\n    0,\n    Math.min(1, (initialVal - min) / (max - min)),\n  );\n  const thumbX = useMotionValue(minX + initialRatio * trackTravel);\n\n  const pointerDown = useMotionValue(0);\n  const forceActiveMotion = useMotionValue(forceActive ? 1 : 0);\n\n  useEffect(() => {\n    forceActiveMotion.set(forceActive ? 1 : 0);\n  }, [forceActive, forceActiveMotion]);\n\n  const isUp = useTransform((): number =>\n    forceActiveMotion.get() > 0.5 || pointerDown.get() > 0.5 ? 1 : 0,\n  );\n\n  const blur = useMotionValue(blurLevel);\n  const specularOpacity = useMotionValue(specularOpacityProp);\n  const specularSaturation = useMotionValue(specularSaturationProp);\n  const refractionBase = useMotionValue(refractionLevel);\n\n  useEffect(() => {\n    blur.set(blurLevel);\n  }, [blurLevel, blur]);\n\n  useEffect(() => {\n    specularOpacity.set(specularOpacityProp);\n  }, [specularOpacityProp, specularOpacity]);\n\n  useEffect(() => {\n    specularSaturation.set(specularSaturationProp);\n  }, [specularSaturationProp, specularSaturation]);\n\n  useEffect(() => {\n    refractionBase.set(refractionLevel);\n  }, [refractionLevel, refractionBase]);\n\n  const scaleRatio = useReducibleSpring(\n    useTransform((): number => {\n      const press = isUp.get() > 0.5 ? 0.9 : 0.4;\n      return press * (refractionBase.get() || 0);\n    }),\n    {\n      stiffness: 400,\n      damping: 30,\n      mass: 0.8,\n    },\n    reduceMotion,\n  );\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const trackRef = useRef<HTMLDivElement>(null);\n  const thumbRef = useRef<HTMLDivElement>(null);\n  const isDraggingRef = useRef(false);\n  const trackBoundsRef = useRef<{ left: number; width: number }>({\n    left: 0,\n    width: sliderWidth,\n  });\n\n  const scaleSpring = useReducibleSpring(\n    useTransform(isUp, [0, 1], [SCALE_REST, SCALE_DRAG]),\n    {\n      stiffness: 400,\n      damping: 30,\n      mass: 0.8,\n    },\n    reduceMotion,\n  );\n\n  const backgroundOpacity = useReducibleSpring(\n    useTransform(isUp, [0, 1], [1, 0.1]),\n    {\n      stiffness: 400,\n      damping: 30,\n      mass: 0.8,\n    },\n    reduceMotion,\n  );\n\n  const seekThumb = useCallback(\n    (targetX: number) => {\n      if (reduceMotion) {\n        thumbX.jump(targetX);\n        return;\n      }\n      animate(thumbX, targetX, {\n        type: \"spring\",\n        stiffness: 450,\n        damping: 32,\n        mass: 0.75,\n      });\n    },\n    [reduceMotion, thumbX],\n  );\n\n  const updatePositionFromClientX = useCallback(\n    (clientX: number, animateSpring = false) => {\n      const { left, width } = trackBoundsRef.current;\n      const effectiveTravel = width - thumbWidthRest;\n      if (effectiveTravel <= 0) return;\n\n      const clickX = clientX - left;\n      const ratio = Math.max(\n        0,\n        Math.min(1, (clickX - thumbWidthRest / 2) / effectiveTravel),\n      );\n\n      const rawVal = min + ratio * (max - min);\n      const steppedVal =\n        step > 0 ? Math.round(rawVal / step) * step : Math.round(rawVal);\n      const nextVal = Math.max(min, Math.min(max, steppedVal));\n\n      const normalizedRatio = (nextVal - min) / (max - min);\n      const targetX = minX + normalizedRatio * trackTravel;\n\n      if (animateSpring) {\n        seekThumb(targetX);\n      } else {\n        thumbX.set(targetX);\n      }\n\n      if (valueMotion.get() !== nextVal) {\n        valueMotion.set(nextVal);\n        onChange?.(nextVal);\n      }\n    },\n    [\n      thumbWidthRest,\n      min,\n      max,\n      step,\n      trackTravel,\n      minX,\n      thumbX,\n      seekThumb,\n      valueMotion,\n      onChange,\n    ],\n  );\n\n  useEffect(() => {\n    const curVal =\n      controlledValue !== undefined ? controlledValue : valueMotion.get();\n    const ratio = Math.max(0, Math.min(1, (curVal - min) / (max - min)));\n    thumbX.set(minX + ratio * trackTravel);\n  }, [size, minX, trackTravel, min, max, controlledValue, thumbX, valueMotion]);\n\n  useEffect(() => {\n    if (!isDraggingRef.current && controlledValue !== undefined) {\n      valueMotion.set(controlledValue);\n      const ratio = Math.max(\n        0,\n        Math.min(1, (controlledValue - min) / (max - min)),\n      );\n      seekThumb(minX + ratio * trackTravel);\n    }\n  }, [controlledValue, min, max, minX, trackTravel, seekThumb, valueMotion]);\n\n  const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (disabled || !trackRef.current) return;\n\n    const rect = trackRef.current.getBoundingClientRect();\n    trackBoundsRef.current = { left: rect.left, width: rect.width };\n\n    isDraggingRef.current = true;\n    pointerDown.set(1);\n\n    e.currentTarget.setPointerCapture(e.pointerId);\n    updatePositionFromClientX(e.clientX, e.target !== thumbRef.current);\n  };\n\n  const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!isDraggingRef.current || disabled) return;\n    updatePositionFromClientX(e.clientX, false);\n  };\n\n  const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!isDraggingRef.current) return;\n    isDraggingRef.current = false;\n    pointerDown.set(0);\n\n    try {\n      e.currentTarget.releasePointerCapture(e.pointerId);\n    } catch {}\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    if (disabled) return;\n    let nextVal = valueMotion.get();\n    const jump = step || 1;\n\n    if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") {\n      e.preventDefault();\n      nextVal = Math.min(max, nextVal + jump);\n    } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") {\n      e.preventDefault();\n      nextVal = Math.max(min, nextVal - jump);\n    } else if (e.key === \"PageUp\") {\n      e.preventDefault();\n      nextVal = Math.min(max, nextVal + jump * 10);\n    } else if (e.key === \"PageDown\") {\n      e.preventDefault();\n      nextVal = Math.max(min, nextVal - jump * 10);\n    } else if (e.key === \"Home\") {\n      e.preventDefault();\n      nextVal = min;\n    } else if (e.key === \"End\") {\n      e.preventDefault();\n      nextVal = max;\n    } else {\n      return;\n    }\n\n    valueMotion.set(nextVal);\n    onChange?.(nextVal);\n    const ratio = (nextVal - min) / (max - min);\n    seekThumb(minX + ratio * trackTravel);\n  };\n\n  const backdropStyle =\n    material === \"liquid\"\n      ? `url(#${filterId}) blur(14px)`\n      : material === \"frosted\"\n        ? \"blur(24px) saturate(180%)\"\n        : \"blur(8px) saturate(120%)\";\n\n  return (\n    <div\n      className={cn(\n        \"relative flex items-center justify-center select-none py-6\",\n        disabled && \"opacity-50 pointer-events-none\",\n        className,\n      )}\n    >\n      <motion.div\n        ref={containerRef}\n        tabIndex={0}\n        role=\"slider\"\n        aria-valuemin={min}\n        aria-valuemax={max}\n        aria-valuenow={ariaValue}\n        aria-label={label}\n        aria-disabled={disabled || undefined}\n        onKeyDown={handleKeyDown}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={handlePointerUp}\n        className=\"outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 rounded-full touch-none select-none cursor-pointer\"\n        style={{\n          position: \"relative\",\n          width: sliderWidth,\n          height: thumbHeight,\n        }}\n      >\n        <div\n          ref={trackRef}\n          style={{\n            display: \"inline-block\",\n            width: sliderWidth,\n            height: sliderHeight,\n            left: 0,\n            top: (thumbHeight - sliderHeight) / 2,\n            backgroundColor: \"#89898F66\",\n            borderRadius: sliderHeight / 2,\n            position: \"absolute\",\n          }}\n        >\n          <div className=\"w-full h-full overflow-hidden rounded-full\">\n            <motion.div\n              style={{\n                top: 0,\n                left: 0,\n                height: sliderHeight,\n                width: useTransform(\n                  thumbX,\n                  (x) =>\n                    `${Math.max(\n                      0,\n                      Math.min(sliderWidth, x + thumbWidth / 2),\n                    )}px`,\n                ),\n                borderRadius: sliderHeight / 2,\n                backgroundColor: currentTheme.fill,\n                boxShadow: `0 0 16px ${currentTheme.glow}`,\n              }}\n            />\n          </div>\n        </div>\n\n        {material === \"liquid\" && (\n          <Filter\n            id={filterId}\n            blur={blur}\n            scaleRatio={scaleRatio}\n            specularOpacity={specularOpacity}\n            specularSaturation={specularSaturation}\n            width={thumbWidth}\n            height={thumbHeight}\n            radius={thumbRadius}\n            bezelWidth={bezelWidth}\n          />\n        )}\n\n        <motion.div\n          ref={thumbRef}\n          className=\"absolute pointer-events-none\"\n          style={{\n            height: thumbHeight,\n            width: thumbWidth,\n            top: 0,\n            x: thumbX,\n            borderRadius: thumbRadius,\n            backdropFilter: backdropStyle,\n            WebkitBackdropFilter: backdropStyle,\n            scale: scaleSpring,\n            willChange: \"transform\",\n            backgroundColor: useTransform(\n              backgroundOpacity,\n              (op) => `rgba(255, 255, 255, ${op})`,\n            ),\n            boxShadow:\n              \"0 4px 18px rgba(0, 0, 0, 0.22), inset 0 1px 1.5px rgba(255, 255, 255, 0.8), inset 0 -1px 2px rgba(0, 0, 0, 0.12)\",\n            border: \"1px solid rgba(255, 255, 255, 0.35)\",\n          }}\n        >\n          <div\n            className=\"pointer-events-none absolute inset-x-0 top-0 h-1/2 opacity-40\"\n            style={{\n              borderRadius: `${thumbRadius}px ${thumbRadius}px 0 0`,\n              background:\n                \"linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0.05) 100%)\",\n            }}\n          />\n        </motion.div>\n      </motion.div>\n    </div>\n  );\n};\n\nexport const Slider = MacSlider;\n",
      "type": "registry:ui",
      "target": "@ui/mac-slider.tsx"
    }
  ]
}
