{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scrub-bar",
  "title": "Scrub Bar",
  "description": "The seek control on its own: a slider that happens to be a timeline, announced in minutes and seconds.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/scrub-bar/scrub-bar.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type ScrubBarProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onChange\" | \"defaultValue\"\n> & {\n  /** Seconds. */\n  duration: number\n  value?: number\n  defaultValue?: number\n  onValueChange?: (seconds: number) => void\n  /** Called once, when the drag ends. */\n  onCommit?: (seconds: number) => void\n  /** How far arrow keys move, in seconds. */\n  step?: number\n  /** Already downloaded, in seconds. Drawn behind the played part. */\n  buffered?: number\n  label?: string\n  disabled?: boolean\n}\n\nfunction clock(seconds: number) {\n  const whole = Math.max(0, Math.round(seconds))\n  return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, \"0\")}`\n}\n\n/**\n * The seek control on its own: a slider that happens to be a timeline. It is a\n * real slider to the keyboard and to assistive technology, announced in\n * minutes and seconds rather than as a number between nought and one.\n */\nexport function ScrubBar({\n  duration,\n  value,\n  defaultValue = 0,\n  onValueChange,\n  onCommit,\n  step = 5,\n  buffered,\n  label = \"Seek\",\n  disabled = false,\n  className,\n  ...rootProps\n}: ScrubBarProps) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultValue)\n  const at = Math.min(Math.max(value ?? uncontrolled, 0), duration)\n  const trackRef = React.useRef<HTMLDivElement>(null)\n  const dragging = React.useRef(false)\n\n  const set = (seconds: number, commit = false) => {\n    const next = Math.min(Math.max(seconds, 0), duration)\n    if (value === undefined) setUncontrolled(next)\n    onValueChange?.(next)\n    if (commit) onCommit?.(next)\n  }\n\n  const secondsAt = (clientX: number) => {\n    const track = trackRef.current\n    if (!track) return at\n\n    const box = track.getBoundingClientRect()\n    if (box.width === 0) return at\n\n    return ((clientX - box.left) / box.width) * duration\n  }\n\n  const portion = duration > 0 ? at / duration : 0\n  const ready =\n    buffered === undefined ? undefined : Math.min(buffered, duration)\n\n  return (\n    <div\n      data-slot=\"scrub-bar\"\n      className={cn(\"flex items-center gap-3\", className)}\n      {...rootProps}\n    >\n      <span className=\"text-muted-foreground shrink-0 font-mono text-xs tabular-nums\">\n        {clock(at)}\n      </span>\n\n      <div\n        ref={trackRef}\n        role=\"slider\"\n        aria-label={label}\n        aria-valuemin={0}\n        aria-valuemax={Math.round(duration)}\n        aria-valuenow={Math.round(at)}\n        aria-valuetext={`${clock(at)} of ${clock(duration)}`}\n        aria-disabled={disabled || undefined}\n        tabIndex={disabled ? -1 : 0}\n        className=\"focus-visible:ring-ring group relative h-6 min-w-0 flex-1 cursor-pointer touch-none focus-visible:ring-2 focus-visible:outline-none\"\n        onPointerDown={(event) => {\n          if (disabled) return\n          dragging.current = true\n          event.currentTarget.setPointerCapture(event.pointerId)\n          set(secondsAt(event.clientX))\n        }}\n        onPointerMove={(event) => {\n          if (!dragging.current) return\n          set(secondsAt(event.clientX))\n        }}\n        onPointerUp={(event) => {\n          if (!dragging.current) return\n          dragging.current = false\n          event.currentTarget.releasePointerCapture(event.pointerId)\n          set(secondsAt(event.clientX), true)\n        }}\n        onKeyDown={(event) => {\n          if (disabled) return\n          const jump: Record<string, number> = {\n            ArrowLeft: -step,\n            ArrowRight: step,\n            ArrowDown: -step,\n            ArrowUp: step,\n          }\n\n          if (event.key in jump) {\n            event.preventDefault()\n            set(at + jump[event.key]!, true)\n            return\n          }\n          if (event.key === \"Home\") {\n            event.preventDefault()\n            set(0, true)\n          }\n          if (event.key === \"End\") {\n            event.preventDefault()\n            set(duration, true)\n          }\n        }}\n      >\n        <div className=\"bg-muted absolute inset-x-0 top-1/2 h-1.5 -translate-y-1/2 overflow-hidden rounded-full\">\n          {ready !== undefined ? (\n            <div\n              className=\"bg-muted-foreground/25 absolute inset-y-0 left-0\"\n              style={{ width: `${(ready / duration) * 100}%` }}\n            />\n          ) : null}\n          <div\n            className=\"bg-primary absolute inset-y-0 left-0\"\n            style={{ width: `${portion * 100}%` }}\n          />\n        </div>\n\n        <span\n          aria-hidden=\"true\"\n          className=\"bg-primary absolute top-1/2 size-3 -translate-x-1/2 -translate-y-1/2 rounded-full opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100 motion-reduce:transition-none\"\n          style={{ left: `${portion * 100}%` }}\n        />\n      </div>\n\n      <span className=\"text-muted-foreground shrink-0 font-mono text-xs tabular-nums\">\n        {clock(duration)}\n      </span>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "control",
    "slider",
    "audio",
    "media",
    "input"
  ],
  "type": "registry:ui"
}