{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-slider",
  "title": "Glass Slider",
  "description": "A slider whose knob turns to glass while you drag it, for one value or a range.",
  "registryDependencies": [
    "@glassyui-native/glass-provider",
    "@glassyui-native/glass-surface"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-slider.tsx",
      "content": "import { forwardRef, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport {\n  Animated,\n  I18nManager,\n  PanResponder,\n  StyleSheet,\n  View,\n  type AccessibilityActionEvent,\n  type LayoutChangeEvent,\n  type ViewProps,\n} from 'react-native';\nimport { useGlassTheme } from './glass-provider';\nimport { GlassSurface } from './glass-surface';\nimport { GLASS_MOTION, GLASS_RADIUS } from './lib/tokens';\nimport { useControllableState } from './lib/use-controllable-state';\n\nexport type GlassSliderProps = Omit<ViewProps, 'children'> & {\n  defaultValue?: number[];\n  disabled?: boolean;\n  max?: number;\n  min?: number;\n  onValueChange?: (value: number[]) => void;\n  /** Hears the values once a drag ends, or after a screen reader moves a thumb. */\n  onValueCommit?: (value: number[]) => void;\n  step?: number;\n  /** Names each thumb in value order. Defaults to the slider's accessibilityLabel. */\n  thumbLabels?: string[];\n  value?: number[];\n};\n\nconst THUMB = { height: 24, width: 38 } as const;\n\n// The 24-point slider and its thumbs answer a touch up to 10 points above or below: 44 in all.\nconst HIT_SLOP = { bottom: 10, top: 10 } as const;\n\nconst THUMB_HIT_SLOP = { bottom: 10, left: 3, right: 3, top: 10 } as const;\n\nconst KNOB_SHADOW = '0px 2px 6px rgba(28, 28, 30, 0.16), 0px 1px 2px rgba(28, 28, 30, 0.1)';\n\nconst ADJUST_ACTIONS = [{ name: 'increment' }, { name: 'decrement' }];\n\nfunction clamp(value: number, low: number, high: number) {\n  return Math.min(high, Math.max(low, value));\n}\n\n// Rounds to the step's own decimals, so steps of 0.1 do not drift, as Radix does.\nfunction snapToStep(value: number, min: number, step: number) {\n  const decimals = (String(step).split('.')[1] ?? '').length;\n\n  return Number((Math.round((value - min) / step) * step + min).toFixed(decimals));\n}\n\n// The thumb nearest a value; the first of several stacked on one value.\nfunction nearestThumb(values: readonly number[], target: number) {\n  let nearest = 0;\n\n  values.forEach((value, index) => {\n    if (Math.abs(value - target) < Math.abs(values[nearest] - target)) nearest = index;\n  });\n\n  return nearest;\n}\n\n// Moves one thumb, stopped by its neighbours so thumbs never cross.\nfunction moveThumb(values: readonly number[], index: number, target: number) {\n  const low = index > 0 ? values[index - 1] : Number.NEGATIVE_INFINITY;\n  const high = index < values.length - 1 ? values[index + 1] : Number.POSITIVE_INFINITY;\n  const next = [...values];\n\n  next[index] = clamp(target, low, high);\n\n  return next;\n}\n\nfunction thumbName(\n  index: number,\n  count: number,\n  sliderLabel: string | undefined,\n  thumbLabels: string[] | undefined,\n) {\n  const own = thumbLabels?.[index];\n\n  if (own !== undefined || count === 1) return own ?? sliderLabel;\n\n  const position = count === 2 ? (index === 0 ? 'minimum' : 'maximum') : `${index + 1} of ${count}`;\n\n  return sliderLabel === undefined ? position : `${sliderLabel}, ${position}`;\n}\n\n/** How a touch drives the slider. Every method reads the slider's latest props. */\ntype SliderGesture = {\n  /** Moves the grabbed thumb with the finger, `dx` points from where the touch began. */\n  drag: (dx: number) => void;\n  drop: () => void;\n  enabled: () => boolean;\n  /** A touch on a thumb takes it from where it is. */\n  grabThumb: (index: number) => void;\n  /** A touch on the track takes the nearest thumb and puts it under the finger. */\n  grabTrack: (x: number) => void;\n};\n\ntype Drag = {\n  before: readonly number[];\n  /** Whether the drag has picked its thumb from any stacked on one value. */\n  decided: boolean;\n  index: number;\n  origin: number;\n  values: readonly number[];\n};\n\n// How far a finger moves before the slider reads which way it is going.\nconst SLOP = 6;\n\n/**\n * A responder that takes a touch only once it slides sideways, so a vertical swipe that starts\n * on the slider still scrolls the page. A touch that lifts without moving runs `onTap`.\n */\nfunction createSlideResponder(\n  gesture: SliderGesture,\n  grab: (originX: number) => void,\n  onTap?: (originX: number) => void,\n) {\n  let sliding = false;\n  let originX = 0;\n\n  return PanResponder.create({\n    onPanResponderGrant: (event) => {\n      sliding = false;\n      originX = event.nativeEvent.locationX;\n    },\n    onPanResponderMove: (_event, state) => {\n      if (!sliding) {\n        if (Math.abs(state.dx) < SLOP || Math.abs(state.dx) < Math.abs(state.dy)) return;\n\n        sliding = true;\n        grab(originX);\n      }\n\n      gesture.drag(state.dx);\n    },\n    onPanResponderRelease: (_event, state) => {\n      if (!sliding && Math.abs(state.dx) < SLOP && Math.abs(state.dy) < SLOP) onTap?.(originX);\n\n      gesture.drop();\n    },\n    onPanResponderTerminate: gesture.drop,\n    // Until the touch slides, a scroll view may take it.\n    onPanResponderTerminationRequest: () => !sliding,\n    // Otherwise Android keeps the touch from the scroll view around the slider.\n    onShouldBlockNativeResponder: () => false,\n    onStartShouldSetPanResponder: gesture.enabled,\n  });\n}\n\ntype SliderThumbProps = {\n  active: boolean;\n  disabled: boolean;\n  gesture: SliderGesture;\n  index: number;\n  label: string | undefined;\n  max: number;\n  min: number;\n  onAdjust: (index: number, direction: 1 | -1) => void;\n  /** Where the thumb sits, from 0 at the track's start to 1 at its end. */\n  position: number;\n  value: number;\n};\n\n// A white knob that turns to clear glass while a finger drags it, as GlassSwitch's does.\nfunction SliderThumb({\n  active,\n  disabled,\n  gesture,\n  index,\n  label,\n  max,\n  min,\n  onAdjust,\n  position,\n  value,\n}: SliderThumbProps) {\n  const { colors, preferences } = useGlassTheme();\n  const [pressed] = useState(() => new Animated.Value(0));\n\n  const [responder] = useState(() => createSlideResponder(gesture, () => gesture.grabThumb(index)));\n\n  useEffect(() => {\n    Animated.timing(pressed, {\n      duration: active ? 120 : 260,\n      toValue: active ? 1 : 0,\n      useNativeDriver: GLASS_MOTION.nativeDriver,\n    }).start();\n  }, [active, pressed]);\n\n  const handleAction = useCallback(\n    (event: AccessibilityActionEvent) => {\n      if (event.nativeEvent.actionName === 'increment') onAdjust(index, 1);\n\n      if (event.nativeEvent.actionName === 'decrement') onAdjust(index, -1);\n    },\n    [index, onAdjust],\n  );\n\n  return (\n    <Animated.View\n      accessible\n      accessibilityActions={ADJUST_ACTIONS}\n      accessibilityLabel={label}\n      accessibilityRole=\"adjustable\"\n      accessibilityState={{ disabled }}\n      accessibilityValue={{ max, min, now: value }}\n      hitSlop={THUMB_HIT_SLOP}\n      onAccessibilityAction={handleAction}\n      style={[\n        styles.thumb,\n        {\n          // Inset by its own width along the way, so the thumb never leaves the track.\n          marginStart: -position * THUMB.width,\n          start: `${position * 100}%`,\n          transform: [\n            {\n              scaleX: preferences.reduceMotion\n                ? 1\n                : pressed.interpolate({ inputRange: [0, 1], outputRange: [1, 1.3] }),\n            },\n            {\n              scaleY: preferences.reduceMotion\n                ? 1\n                : pressed.interpolate({ inputRange: [0, 1], outputRange: [1, 1.4] }),\n            },\n          ],\n        },\n      ]}\n      {...responder.panHandlers}\n    >\n      {disabled ? null : (\n        <GlassSurface\n          elevation=\"none\"\n          preset=\"subtle\"\n          style={[StyleSheet.absoluteFill, styles.round]}\n        />\n      )}\n      <Animated.View\n        style={[\n          StyleSheet.absoluteFill,\n          styles.round,\n          {\n            backgroundColor: colors.knob,\n            boxShadow: KNOB_SHADOW,\n            opacity: preferences.reduceTransparency\n              ? 1\n              : pressed.interpolate({ inputRange: [0, 1], outputRange: [1, 0.12] }),\n          },\n        ]}\n      />\n    </Animated.View>\n  );\n}\n\n/**\n * A slider with one thumb or several. A tap or a sideways drag on the track takes the nearest\n * thumb; a vertical swipe scrolls past. Thumbs stop at each other and never cross, and each white\n * knob turns to clear glass while it is dragged.\n */\nexport const GlassSlider = forwardRef<View, GlassSliderProps>(\n  (\n    {\n      accessibilityLabel,\n      defaultValue,\n      disabled = false,\n      max = 100,\n      min = 0,\n      onLayout,\n      onValueChange,\n      onValueCommit,\n      step = 1,\n      style,\n      thumbLabels,\n      value,\n      ...rest\n    },\n    ref,\n  ) => {\n    const { colors } = useGlassTheme();\n    const [values, setValues] = useControllableState(value, defaultValue ?? [min], onValueChange);\n    const [active, setActive] = useState<number | null>(null);\n    const width = useRef(0);\n    const drag = useRef<Drag | null>(null);\n    const latest = useRef({ disabled, max, min, onValueCommit, setValues, step, values });\n\n    // The gesture handlers live as long as the slider, so they read its props from here.\n    useLayoutEffect(() => {\n      latest.current = { disabled, max, min, onValueCommit, setValues, step, values };\n    });\n\n    const [gesture] = useState<SliderGesture>(() => {\n      const travel = () => Math.max(width.current - THUMB.width, 0);\n\n      // A thumb's centre, in points from the slider's left edge.\n      const centreOf = (thumb: number) => {\n        const { max: high, min: low } = latest.current;\n        const along = high > low ? clamp((thumb - low) / (high - low), 0, 1) : 0;\n\n        return THUMB.width / 2 + (I18nManager.isRTL ? 1 - along : along) * travel();\n      };\n\n      // The value whose thumb centre sits at `x`.\n      const valueAt = (x: number) => {\n        const { max: high, min: low, step: increment } = latest.current;\n        const across = clamp((x - THUMB.width / 2) / Math.max(travel(), 1), 0, 1);\n        const along = I18nManager.isRTL ? 1 - across : across;\n\n        return clamp(snapToStep(low + along * (high - low), low, increment), low, high);\n      };\n\n      const begin = (index: number, origin: number) => {\n        const current = latest.current.values;\n\n        drag.current = { before: current, decided: false, index, origin, values: current };\n        setActive(index);\n      };\n\n      const move = (dx: number) => {\n        const current = drag.current;\n\n        if (!current) return;\n\n        const target = valueAt(current.origin + dx);\n        const held = current.values[current.index];\n        const decides = !current.decided && target !== held;\n        let index = current.index;\n\n        // Thumbs stacked on one value hand the drag, once, to whichever can follow the finger.\n        // After that the thumb stops at its neighbour, so a drag never passes one thumb through\n        // another.\n        if (decides) {\n          while (index > 0 && target < held && current.values[index - 1] === held) index -= 1;\n\n          while (\n            index < current.values.length - 1 &&\n            target > held &&\n            current.values[index + 1] === held\n          ) {\n            index += 1;\n          }\n        }\n\n        const next = moveThumb(current.values, index, target);\n\n        if (index !== current.index) setActive(index);\n\n        drag.current = { ...current, decided: current.decided || decides, index, values: next };\n\n        if (next[index] !== current.values[index]) latest.current.setValues(next);\n      };\n\n      return {\n        drag: move,\n        drop: () => {\n          const current = drag.current;\n\n          if (!current) return;\n\n          drag.current = null;\n          setActive(null);\n\n          if (current.values.some((thumb, index) => thumb !== current.before[index])) {\n            latest.current.onValueCommit?.([...current.values]);\n          }\n        },\n        enabled: () => !latest.current.disabled,\n        grabThumb: (index) => begin(index, centreOf(latest.current.values[index])),\n        grabTrack: (x) => {\n          begin(nearestThumb(latest.current.values, valueAt(x)), x);\n          move(0);\n        },\n      };\n    });\n\n    // A tap on the track puts the nearest thumb under the finger; a slide drags it from there.\n    const [responder] = useState(() =>\n      createSlideResponder(gesture, gesture.grabTrack, gesture.grabTrack),\n    );\n\n    const handleLayout = useCallback(\n      (event: LayoutChangeEvent) => {\n        width.current = event.nativeEvent.layout.width;\n        onLayout?.(event);\n      },\n      [onLayout],\n    );\n\n    const adjust = useCallback(\n      (index: number, direction: 1 | -1) => {\n        if (disabled) return;\n\n        const target = clamp(snapToStep(values[index] + direction * step, min, step), min, max);\n        const next = moveThumb(values, index, target);\n\n        if (next[index] === values[index]) return;\n\n        setValues(next);\n        onValueCommit?.(next);\n      },\n      [disabled, max, min, onValueCommit, setValues, step, values],\n    );\n\n    const positionOf = (thumb: number) =>\n      max > min ? clamp((thumb - min) / (max - min), 0, 1) : 0;\n\n    const rangeStart = values.length > 1 ? positionOf(values[0]) : 0;\n    const rangeEnd = positionOf(values[values.length - 1] ?? min);\n\n    return (\n      <View\n        ref={ref}\n        hitSlop={HIT_SLOP}\n        onLayout={handleLayout}\n        style={[styles.root, disabled && styles.disabled, style]}\n        {...responder.panHandlers}\n        {...rest}\n      >\n        <View style={[styles.track, { backgroundColor: colors.track }]}>\n          <View\n            style={[\n              styles.range,\n              {\n                backgroundColor: colors.accent,\n                end: `${(1 - rangeEnd) * 100}%`,\n                start: `${rangeStart * 100}%`,\n              },\n            ]}\n          />\n        </View>\n        {values.map((thumb, index) => (\n          <SliderThumb\n            key={index}\n            active={active === index}\n            disabled={disabled}\n            gesture={gesture}\n            index={index}\n            label={thumbName(index, values.length, accessibilityLabel, thumbLabels)}\n            max={max}\n            min={min}\n            onAdjust={adjust}\n            position={positionOf(thumb)}\n            value={thumb}\n          />\n        ))}\n      </View>\n    );\n  },\n);\n\nGlassSlider.displayName = 'GlassSlider';\n\nconst styles = StyleSheet.create({\n  disabled: { opacity: 0.45 },\n  range: { borderRadius: GLASS_RADIUS.capsule, bottom: 0, position: 'absolute', top: 0 },\n  root: { alignSelf: 'stretch', height: THUMB.height, justifyContent: 'center' },\n  round: { borderCurve: 'continuous', borderRadius: GLASS_RADIUS.capsule },\n  thumb: {\n    borderCurve: 'continuous',\n    borderRadius: GLASS_RADIUS.capsule,\n    height: THUMB.height,\n    position: 'absolute',\n    top: 0,\n    width: THUMB.width,\n  },\n  track: {\n    borderRadius: GLASS_RADIUS.capsule,\n    height: 6,\n    overflow: 'hidden',\n    pointerEvents: 'none',\n  },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-slider.tsx"
    }
  ],
  "type": "registry:ui"
}