{
  "name": "progress-bar",
  "type": "registry:ui",
  "description": "Inline progress bar with determinate and indeterminate modes, segments and a value readout",
  "dependencies": [
    "remotion"
  ],
  "registryDependencies": [
    "motion-primitive",
    "motion-tokens",
    "timing",
    "layout"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/progress-bar.tsx",
      "type": "registry:ui",
      "content": "import { interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { scaleFont } from \"@/remotion/lib/layout\";\nimport {\n  resolveSpringConfig,\n  type MotionSpring,\n} from \"@/remotion/lib/motion-primitive\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\nimport { EASING_ENTER } from \"@/remotion/lib/timing\";\n\nexport type ProgressBarProps = {\n  /** Value the bar fills to, 0–1. */\n  progress?: number;\n  /** Value the bar starts from, 0–1. */\n  from?: number;\n  durationInFrames?: number;\n  delayInFrames?: number;\n  /** Drive the fill with a spring instead of the ease-out curve. */\n  spring?: MotionSpring;\n  /** Loop a shuttle across the track — work with no known end. */\n  indeterminate?: boolean;\n  label?: string;\n  /** Percentage readout on the right of the label row. */\n  showValue?: boolean;\n  /** Override the readout text. */\n  formatValue?: (progress: number) => string;\n  /** Divide the track into equal steps. */\n  segments?: number;\n  color?: string;\n  trackColor?: string;\n  labelColor?: string;\n  /** Bar thickness. Scales with the frame by default. */\n  height?: number;\n  /** Corner radius. Defaults to a full round cap. */\n  radius?: number;\n  /** Soft light carried by the leading edge of the fill. */\n  glow?: boolean;\n  /** Width of the whole control. */\n  width?: number | string;\n  style?: React.CSSProperties;\n};\n\n/** Seconds for one pass of the indeterminate shuttle. */\nconst SHUTTLE_SECONDS = 1.6;\nconst SHUTTLE_WIDTH = 0.34;\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n/**\n * A bar that fills.\n *\n * It lays out inline rather than owning the frame, so it can sit in a card, a\n * row of stats or a render queue — a primitive that fills the composition\n * cannot be composed with anything.\n *\n * The fill decelerates and the leading edge carries its own light, which is\n * what separates progress from a rectangle changing width.\n */\nexport const ProgressBar: React.FC<ProgressBarProps> = ({\n  progress = 1,\n  from = 0,\n  durationInFrames = 60,\n  delayInFrames = 0,\n  spring: springProp,\n  indeterminate = false,\n  label,\n  showValue = false,\n  formatValue,\n  segments,\n  color = \"#e8b86d\",\n  trackColor = \"rgba(255, 255, 255, 0.08)\",\n  labelColor = \"rgba(248, 250, 252, 0.55)\",\n  height: heightProp,\n  radius,\n  glow = true,\n  width: widthProp = \"100%\",\n  style,\n}) => {\n  const frame = useCurrentFrame();\n  const { width: frameWidth, fps } = useVideoConfig();\n\n  const height = heightProp ?? Math.max(6, scaleFont(12, frameWidth));\n  const corner = radius ?? height;\n  const labelSize = scaleFont(32, frameWidth);\n\n  const eased = springProp\n    ? spring({\n        frame,\n        fps,\n        config: resolveSpringConfig(springProp),\n        delay: delayInFrames,\n        durationInFrames,\n      })\n    : interpolate(\n        frame,\n        [delayInFrames, delayInFrames + durationInFrames],\n        [0, 1],\n        { easing: EASING_ENTER, ...clamp },\n      );\n\n  const value = interpolate(eased, [0, 1], [from, progress], clamp);\n\n  /* The shuttle eases at both ends of its pass, so it reads as a sweep rather\n   * than a block sliding at constant speed. */\n  const cycle = (frame % (SHUTTLE_SECONDS * fps)) / (SHUTTLE_SECONDS * fps);\n  const shuttleLeft =\n    interpolate(cycle, [0, 1], [-SHUTTLE_WIDTH, 1], {\n      easing: EASING.editorial,\n      ...clamp,\n    }) * 100;\n\n  const readout = formatValue\n    ? formatValue(value)\n    : `${Math.round(value * 100)}%`;\n\n  return (\n    <div style={{ width: widthProp, ...style }}>\n      {label || showValue ? (\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"baseline\",\n            justifyContent: \"space-between\",\n            gap: labelSize,\n            marginBottom: height,\n            color: labelColor,\n            fontSize: labelSize,\n            fontWeight: 500,\n            letterSpacing: \"0.02em\",\n          }}\n        >\n          <span>{label}</span>\n          {showValue ? (\n            <span style={{ fontVariantNumeric: \"tabular-nums\" }}>\n              {indeterminate ? \"\" : readout}\n            </span>\n          ) : null}\n        </div>\n      ) : null}\n\n      <div\n        style={{\n          position: \"relative\",\n          width: \"100%\",\n          height,\n          backgroundColor: trackColor,\n          borderRadius: corner,\n          overflow: \"hidden\",\n          border: \"1px solid rgba(255, 255, 255, 0.06)\",\n          boxShadow: \"inset 0 1px 3px rgba(0, 0, 0, 0.35)\",\n        }}\n      >\n        {indeterminate ? (\n          <div\n            style={{\n              position: \"absolute\",\n              top: 0,\n              bottom: 0,\n              left: `${shuttleLeft}%`,\n              width: `${SHUTTLE_WIDTH * 100}%`,\n              borderRadius: corner,\n              background: `linear-gradient(90deg, transparent, ${color}, transparent)`,\n            }}\n          />\n        ) : (\n          <div\n            style={{\n              position: \"absolute\",\n              top: 0,\n              bottom: 0,\n              left: 0,\n              width: `${Math.max(0, Math.min(1, value)) * 100}%`,\n              borderRadius: corner,\n              background: color,\n              ...(glow ? { boxShadow: `0 0 ${height * 1.6}px ${color}88` } : {}),\n            }}\n          >\n            {glow ? (\n              <div\n                style={{\n                  position: \"absolute\",\n                  top: 0,\n                  bottom: 0,\n                  right: 0,\n                  width: height * 2.4,\n                  background: `linear-gradient(90deg, transparent, rgba(255,255,255,0.55))`,\n                  opacity: value > 0.002 && value < 0.999 ? 1 : 0,\n                }}\n              />\n            ) : null}\n          </div>\n        )}\n\n        {segments && segments > 1\n          ? Array.from({ length: segments - 1 }, (_, index) => (\n              <div\n                key={`tick-${index}`}\n                style={{\n                  position: \"absolute\",\n                  top: 0,\n                  bottom: 0,\n                  left: `${((index + 1) / segments) * 100}%`,\n                  width: Math.max(1, Math.round(height * 0.16)),\n                  background: \"rgba(0, 0, 0, 0.45)\",\n                }}\n              />\n            ))\n          : null}\n      </div>\n    </div>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "atoms",
    "drive": "time",
    "tier": "core"
  }
}