{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <edu@smoothui.dev>","css":{},"dependencies":["@base-ui/react","motion"],"description":"Animated Progress for SmoothUI with Base UI / Radix twins, determinate and indeterminate states.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Progress as ProgressPrimitive } from \"@base-ui/react/progress\";\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { type ComponentProps, createContext, useContext, useRef } from \"react\";\nimport {\n  EASE_IN_OUT,\n  LOOP_CLASS,\n  LOOP_REPEAT_DELAY,\n  loopTransition,\n  SPRING_DEFAULT,\n  useLoopInView,\n} from \"@/components/smoothui/lib/animation\";\n\nexport type ProgressSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface ProgressProps {\n  /** Accessible name when there is no visible `label` */\n  \"aria-label\"?: string;\n  /** Optional CSS class for the root */\n  className?: string;\n  /** Visible label rendered above the track */\n  label?: string;\n  /** Maximum value (default 100) */\n  max?: number;\n  /** Minimum value (default 0) */\n  min?: number;\n  /** Show the formatted value next to the label */\n  showValue?: boolean;\n  /** Track height: `sm`, `md` (default) or `lg` */\n  size?: ProgressSize;\n  /** Current value. Pass `null` for an indeterminate bar. */\n  value: number | null;\n}\n\nexport type ProgressRootProps = Omit<\n  ComponentProps<typeof ProgressPrimitive.Root>,\n  \"className\"\n> & { className?: string };\nexport type ProgressTrackProps = Omit<\n  ComponentProps<typeof ProgressPrimitive.Track>,\n  \"className\"\n> & { className?: string; size?: ProgressSize };\nexport type ProgressIndicatorProps = Omit<\n  ComponentProps<typeof ProgressPrimitive.Indicator>,\n  \"className\" | \"render\"\n> & { className?: string };\nexport type ProgressLabelProps = Omit<\n  ComponentProps<typeof ProgressPrimitive.Label>,\n  \"className\"\n> & { className?: string };\nexport type ProgressValueProps = Omit<\n  ComponentProps<typeof ProgressPrimitive.Value>,\n  \"className\"\n> & { className?: string };\n\nconst TRACK_SIZE: Record<ProgressSize, string> = {\n  lg: \"h-3\",\n  md: \"h-2\",\n  sm: \"h-1\",\n};\n\nconst INDETERMINATE_WIDTH = \"40%\";\nconst INDETERMINATE_DURATION = 1.4;\nconst FULL_PERCENT = 100;\n\n/** Normalized 0–100 percentage, or `null` when indeterminate. */\nconst toPercent = (value: number | null, min: number, max: number) => {\n  if (value === null || max <= min) {\n    return null;\n  }\n  const ratio = ((value - min) / (max - min)) * FULL_PERCENT;\n  return Math.min(FULL_PERCENT, Math.max(0, ratio));\n};\n\nconst ProgressPercentContext = createContext<number | null>(null);\n\n/**\n * Progress root — groups label, value, and track.\n * Renders `role=\"progressbar\"` with value semantics from Base UI.\n */\nexport const ProgressRoot = ({\n  className,\n  max = FULL_PERCENT,\n  min = 0,\n  value,\n  ...props\n}: ProgressRootProps) => (\n  <ProgressPercentContext.Provider value={toPercent(value, min, max)}>\n    <ProgressPrimitive.Root\n      className={cn(\"flex w-full min-w-0 flex-col gap-2\", className)}\n      data-slot=\"progress\"\n      max={max}\n      min={min}\n      value={value}\n      {...props}\n    />\n  </ProgressPercentContext.Provider>\n);\n\nexport const ProgressTrack = ({\n  className,\n  size = \"md\",\n  ...props\n}: ProgressTrackProps) => (\n  <ProgressPrimitive.Track\n    className={cn(\n      \"relative w-full min-w-0 overflow-hidden rounded-full bg-foreground/40\",\n      TRACK_SIZE[size],\n      className\n    )}\n    data-slot=\"progress-track\"\n    {...props}\n  />\n);\n\n/**\n * Animated fill. Determinate: full-width bar clipped via `translateX` so\n * rounded caps stay circular. Indeterminate: a sliding bar. Both are\n * transform-only; reduced motion is instant / static.\n */\nexport const ProgressIndicator = ({\n  className,\n  ...props\n}: ProgressIndicatorProps) => {\n  const percent = useContext(ProgressPercentContext);\n  const shouldReduceMotion = useReducedMotion();\n  const indicatorRef = useRef<HTMLDivElement>(null);\n  // The track clips the sliding bar, so watch the track, not the bar.\n  const inView = useLoopInView(indicatorRef, { observeParent: true });\n  const isIndeterminate = percent === null;\n  const isLooping = isIndeterminate && !shouldReduceMotion && inView;\n\n  let animate: { opacity?: number; transform: string | string[] };\n  let initial: { opacity?: number; transform: string } | false = false;\n  let transition: object = SPRING_DEFAULT;\n\n  if (isIndeterminate) {\n    if (shouldReduceMotion) {\n      // Full bar at low opacity — a static 40%-wide bar reads as determinate\n      animate = { opacity: 0.4, transform: \"translateX(0%)\" };\n      initial = { opacity: 0.4, transform: \"translateX(0%)\" };\n      transition = { duration: 0 };\n    } else if (inView) {\n      animate = { transform: [\"translateX(-100%)\", \"translateX(250%)\"] };\n      initial = { transform: \"translateX(-100%)\" };\n      transition = loopTransition(\n        { duration: INDETERMINATE_DURATION, ease: EASE_IN_OUT },\n        LOOP_REPEAT_DELAY.progress\n      );\n    } else {\n      // Off screen: rest at the start instead of looping unseen.\n      animate = { transform: \"translateX(-100%)\" };\n      initial = { transform: \"translateX(-100%)\" };\n      transition = { duration: 0 };\n    }\n  } else {\n    // translateX keeps rounded ends; scaleX would squash them into ellipses\n    animate = {\n      transform: `translateX(${(percent ?? 0) - FULL_PERCENT}%)`,\n    };\n    if (shouldReduceMotion) {\n      transition = { duration: 0 };\n    }\n  }\n\n  return (\n    <ProgressPrimitive.Indicator\n      className={cn(\n        \"block h-full rounded-full bg-brand\",\n        isLooping && LOOP_CLASS,\n        className\n      )}\n      data-slot=\"progress-indicator\"\n      render={\n        <motion.div\n          animate={animate}\n          initial={initial}\n          ref={indicatorRef}\n          style={{\n            width: isIndeterminate\n              ? shouldReduceMotion\n                ? \"100%\"\n                : INDETERMINATE_WIDTH\n              : \"100%\",\n          }}\n          transition={transition}\n        />\n      }\n      {...props}\n    />\n  );\n};\n\nexport const ProgressLabel = ({ className, ...props }: ProgressLabelProps) => (\n  <ProgressPrimitive.Label\n    className={cn(\"font-medium text-sm leading-none\", className)}\n    data-slot=\"progress-label\"\n    {...props}\n  />\n);\n\nexport const ProgressValue = ({ className, ...props }: ProgressValueProps) => (\n  <ProgressPrimitive.Value\n    className={cn(\"text-muted-foreground text-sm tabular-nums\", className)}\n    data-slot=\"progress-value\"\n    {...props}\n  />\n);\n\n/**\n * SmoothUI Progress — Base UI twin (default).\n * Convenience API; use the compound parts for custom layouts.\n */\nexport default function Progress({\n  \"aria-label\": ariaLabel,\n  className,\n  label,\n  max,\n  min,\n  showValue = false,\n  size = \"md\",\n  value,\n}: ProgressProps) {\n  const hasHeader = Boolean(label) || (showValue && value !== null);\n\n  return (\n    <ProgressRoot\n      aria-label={label ? undefined : ariaLabel}\n      className={className}\n      max={max}\n      min={min}\n      value={value}\n    >\n      {hasHeader ? (\n        <div className=\"flex items-center justify-between gap-2\">\n          {label ? <ProgressLabel>{label}</ProgressLabel> : <span />}\n          {showValue && value !== null ? <ProgressValue /> : null}\n        </div>\n      ) : null}\n      <ProgressTrack size={size}>\n        <ProgressIndicator />\n      </ProgressTrack>\n    </ProgressRoot>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/progress/index.tsx","type":"registry:ui"}],"name":"progress","registryDependencies":["https://smoothui.dev/r/lib.json","https://smoothui.dev/r/tokens.json"],"title":"Progress","type":"registry:ui"}