{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-progress",
  "title": "Glass Progress",
  "description": "A glass track with a fill that slides to its value, or sweeps when the length is unknown.",
  "registryDependencies": [
    "@glassyui-native/glass-provider"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-progress.tsx",
      "content": "import { forwardRef, useCallback, useEffect, useState, type ReactNode } from 'react';\nimport {\n  Animated,\n  Easing,\n  I18nManager,\n  StyleSheet,\n  Text,\n  View,\n  type LayoutChangeEvent,\n  type ViewProps,\n} from 'react-native';\nimport { useGlassTheme } from './glass-provider';\nimport { plainTextOf, wrapPlainText } from './lib/plain-text';\nimport { GLASS_MOTION, GLASS_RADIUS } from './lib/tokens';\nimport { useLoop } from './lib/use-loop';\n\nexport type GlassProgressSize = 'sm' | 'md' | 'lg';\n\nexport type GlassProgressProps = Omit<ViewProps, 'children'> & {\n  /** Text above the bar. It also names the bar for assistive technology. */\n  label?: ReactNode;\n  /** The value that means done. Defaults to 100. */\n  max?: number;\n  /** Shows the percentage beside the label. */\n  showValue?: boolean;\n  size?: GlassProgressSize;\n  /** How far along. Leave it out, or pass null, when the length is unknown. */\n  value?: number | null;\n};\n\nconst TRACK_HEIGHT = { sm: 4, md: 8, lg: 12 } as const;\n\n// The web's ease-out curve, for the fill sliding to a new value.\nconst EASE_OUT = Easing.bezier(0.32, 0.72, 0, 1);\n\nconst SLIDE_MS = 640;\n\n// Without a value, a segment 40% as wide as the track sweeps across it and starts again.\nconst SEGMENT = 0.4;\n\nconst SWEEP_MS = 1600;\n\n/**\n * A track with a fill that slides to its value. Without a value a segment sweeps along it, for\n * work of unknown length; Reduce Motion holds the segment still in the middle.\n */\nexport const GlassProgress = forwardRef<View, GlassProgressProps>(\n  (\n    { accessibilityLabel, label, max = 100, showValue = false, size = 'md', style, value, ...rest },\n    ref,\n  ) => {\n    const { colors, preferences } = useGlassTheme();\n    const [width, setWidth] = useState(0);\n    const known = value !== null && value !== undefined && Number.isFinite(value);\n    const clamped = known ? Math.min(Math.max(value, 0), max) : 0;\n    const fraction = known && max > 0 ? clamped / max : 0;\n    const percent = Math.round(fraction * 100);\n    const [fill] = useState(() => new Animated.Value(fraction));\n    const sweep = useLoop(SWEEP_MS, !known && !preferences.reduceMotion);\n    // Translation is physical, so a right-to-left layout fills from the right.\n    const direction = I18nManager.isRTL ? -1 : 1;\n    const segment = SEGMENT * width;\n\n    useEffect(() => {\n      if (preferences.reduceMotion) {\n        fill.setValue(fraction);\n\n        return;\n      }\n\n      const animation = Animated.timing(fill, {\n        duration: SLIDE_MS,\n        easing: EASE_OUT,\n        toValue: fraction,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      });\n\n      animation.start();\n\n      return () => animation.stop();\n    }, [fill, fraction, preferences.reduceMotion]);\n\n    const measure = useCallback((event: LayoutChangeEvent) => {\n      setWidth(event.nativeEvent.layout.width);\n    }, []);\n\n    const fillColors = { backgroundColor: colors.accent };\n\n    return (\n      <View\n        ref={ref}\n        accessible\n        accessibilityLabel={accessibilityLabel ?? plainTextOf(label)}\n        accessibilityRole=\"progressbar\"\n        // Native accessibility values are whole numbers, so the bar reports a percentage.\n        accessibilityValue={known ? { max: 100, min: 0, now: percent } : undefined}\n        style={[styles.root, style]}\n        {...rest}\n      >\n        {label || (showValue && known) ? (\n          <View style={styles.header}>\n            {wrapPlainText(label, (text) => (\n              <Text style={[styles.label, { color: colors.foreground }]}>{text}</Text>\n            ))}\n            {showValue && known ? (\n              <Text style={[styles.value, { color: colors.mutedForeground }]}>{percent}%</Text>\n            ) : null}\n          </View>\n        ) : null}\n        <View\n          onLayout={measure}\n          style={[styles.track, { backgroundColor: colors.track, height: TRACK_HEIGHT[size] }]}\n        >\n          {width > 0 && known ? (\n            // The whole pill slides in from the start, so its leading end stays round.\n            <Animated.View\n              style={[\n                StyleSheet.absoluteFill,\n                styles.fill,\n                fillColors,\n                {\n                  transform: [\n                    {\n                      translateX: fill.interpolate({\n                        inputRange: [0, 1],\n                        outputRange: [-width * direction, 0],\n                      }),\n                    },\n                  ],\n                },\n              ]}\n            />\n          ) : null}\n          {width > 0 && !known ? (\n            <Animated.View\n              style={[\n                styles.fill,\n                styles.segment,\n                fillColors,\n                {\n                  transform: [\n                    {\n                      translateX: preferences.reduceMotion\n                        ? 0.75 * segment * direction\n                        : sweep.interpolate({\n                            inputRange: [0, 1],\n                            outputRange: [-segment * direction, width * direction],\n                          }),\n                    },\n                  ],\n                },\n              ]}\n            />\n          ) : null}\n        </View>\n      </View>\n    );\n  },\n);\n\nGlassProgress.displayName = 'GlassProgress';\n\nconst styles = StyleSheet.create({\n  fill: {\n    borderCurve: 'continuous',\n    borderRadius: GLASS_RADIUS.capsule,\n    boxShadow: 'inset 0px 1px 0px rgba(255, 255, 255, 0.4)',\n    experimental_backgroundImage:\n      'linear-gradient(180deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0) 60%)',\n  },\n  header: { alignItems: 'baseline', flexDirection: 'row', gap: 16 },\n  label: { flexShrink: 1, fontSize: 14, lineHeight: 20 },\n  root: { gap: 8, width: '100%' },\n  segment: {\n    bottom: 0,\n    pointerEvents: 'none',\n    position: 'absolute',\n    start: 0,\n    top: 0,\n    width: `${SEGMENT * 100}%`,\n  },\n  track: {\n    borderCurve: 'continuous',\n    borderRadius: GLASS_RADIUS.capsule,\n    boxShadow: 'inset 0px 1px 2px rgba(0, 0, 0, 0.18)',\n    overflow: 'hidden',\n  },\n  value: {\n    fontSize: 14,\n    fontVariant: ['tabular-nums'],\n    lineHeight: 20,\n    marginStart: 'auto',\n  },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-progress.tsx"
    },
    {
      "path": "registry/glassyui-native/lib/plain-text.ts",
      "content": "import { Children, isValidElement, type ReactNode } from 'react';\n\n/**\n * Sets plain text inside a Text, which React Native needs to render it. Content that is all text\n * becomes one Text; beside elements such as icons, each run of text gets its own.\n */\nexport function wrapPlainText(node: ReactNode, render: (text: ReactNode) => ReactNode) {\n  const parts = Children.toArray(node);\n\n  if (parts.length === 0) return null;\n\n  if (parts.every((part) => !isValidElement(part))) return render(node);\n\n  return Children.map(node, (part) => (isValidElement(part) ? part : render(part)));\n}\n\n/** The words in `node` when it is only text, to name a control for assistive technology. */\nexport function plainTextOf(node: ReactNode) {\n  const parts = Children.toArray(node);\n\n  return parts.length > 0 && parts.every((part) => !isValidElement(part))\n    ? parts.join('')\n    : undefined;\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/plain-text.ts"
    },
    {
      "path": "registry/glassyui-native/lib/use-loop.ts",
      "content": "import { useEffect, useState } from 'react';\nimport { Animated, Easing } from 'react-native';\nimport { GLASS_MOTION } from './tokens';\n\n/**\n * A value that runs from 0 to 1 over `duration`, then starts again, for the library's only loops:\n * progress, spinners and skeletons. It rests at 0 while `running` is false and stops on unmount.\n */\nexport function useLoop(duration: number, running: boolean) {\n  // State, not a ref, holds the value: render reads it, and it never changes identity.\n  const [progress] = useState(() => new Animated.Value(0));\n\n  useEffect(() => {\n    progress.setValue(0);\n\n    if (!running) return;\n\n    const loop = Animated.loop(\n      Animated.timing(progress, {\n        duration,\n        easing: Easing.linear,\n        toValue: 1,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      }),\n    );\n\n    loop.start();\n\n    return () => loop.stop();\n  }, [duration, progress, running]);\n\n  return progress;\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/use-loop.ts"
    }
  ],
  "type": "registry:ui"
}