{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "motorized-fader",
  "title": "Motorized fader",
  "description": "A console fader with a motor: when someone else sets it, the cap travels there on its own and says who moved it. Grab it and it's yours. For settings an agent, a teammate or an automation can change too.",
  "dependencies": [
    "cn"
  ],
  "registryDependencies": [
    "https://bevel-ui.vercel.app/r/bevel.json"
  ],
  "files": [
    {
      "path": "registry/bevel/ui/motorized-fader.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\n\ntype MotorizedFaderProps = Omit<React.ComponentProps<\"div\">, \"onChange\" | \"defaultValue\"> & {\n  value: number\n  /** Called when you move it. Set changedBy to you when you take it. */\n  onValueChange?: (value: number) => void\n  min?: number\n  max?: number\n  step?: number\n  /** Who set the current value. Anyone but you, and the motor drives the cap there. */\n  changedBy?: string\n  /** Your name as changedBy shows it. Default \"You\". */\n  you?: string\n  /** How the value reads aloud and on the scribble strip. */\n  format?: (value: number) => string\n  disabled?: boolean\n}\n\n/**\n * A motorized console fader. When someone else (an agent, a teammate, an\n * automation) sets it, the cap travels there on its own and says who moved\n * it. Grab it, or use the arrow keys, and it's yours. Vertical.\n */\nfunction MotorizedFader({\n  className,\n  value,\n  onValueChange,\n  min = 0,\n  max = 100,\n  step = 1,\n  changedBy,\n  you = \"You\",\n  format = String,\n  disabled,\n  ...props\n}: MotorizedFaderProps) {\n  const track = React.useRef<HTMLDivElement>(null)\n  const [dragging, setDragging] = React.useState(false)\n  const t = max === min ? 0 : (Math.min(Math.max(value, min), max) - min) / (max - min)\n  const remote = changedBy !== undefined && changedBy !== you\n\n  const set = (next: number) => {\n    const snapped = Math.min(max, Math.max(min, Math.round((next - min) / step) * step + min))\n    if (snapped !== value) onValueChange?.(snapped)\n  }\n  const fromPointer = (event: React.PointerEvent) => {\n    const box = track.current?.getBoundingClientRect()\n    if (!box) return\n    set(min + (1 - (event.clientY - box.top) / box.height) * (max - min))\n  }\n  const onKeyDown = (event: React.KeyboardEvent) => {\n    const big = Math.max(step, (max - min) / 10)\n    const moves: Record<string, number> = {\n      ArrowUp: value + step,\n      ArrowRight: value + step,\n      ArrowDown: value - step,\n      ArrowLeft: value - step,\n      PageUp: value + big,\n      PageDown: value - big,\n      Home: min,\n      End: max,\n    }\n    if (!(event.key in moves)) return\n    event.preventDefault()\n    set(moves[event.key]!)\n  }\n\n  return (\n    <div\n      role=\"slider\"\n      tabIndex={disabled ? -1 : 0}\n      aria-orientation=\"vertical\"\n      aria-valuemin={min}\n      aria-valuemax={max}\n      aria-valuenow={value}\n      aria-valuetext={`${format(value)}${changedBy ? `, set by ${changedBy}` : \"\"}`}\n      aria-disabled={disabled || undefined}\n      data-slot=\"motorized-fader\"\n      data-remote={remote ? \"\" : undefined}\n      data-dragging={dragging ? \"\" : undefined}\n      data-disabled={disabled ? \"\" : undefined}\n      onKeyDown={disabled ? undefined : onKeyDown}\n      onPointerDown={(event) => {\n        if (disabled || event.button !== 0) return\n        event.currentTarget.setPointerCapture(event.pointerId)\n        setDragging(true)\n        fromPointer(event)\n      }}\n      onPointerMove={(event) => {\n        if (event.currentTarget.hasPointerCapture(event.pointerId)) fromPointer(event)\n      }}\n      onPointerUp={() => setDragging(false)}\n      onLostPointerCapture={() => setDragging(false)}\n      className={cn(\n        \"group relative flex h-[calc(var(--motorized-fader-travel)+var(--motorized-fader-cap))] w-14 touch-none justify-center outline-none select-none data-disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      {/* Scale lines either side of the slot. */}\n      <span aria-hidden className=\"absolute inset-y-[calc(var(--motorized-fader-cap)/2)] inset-x-1 flex flex-col justify-between\">\n        {Array.from({ length: 11 }, (_, i) => (\n          <span key={i} className={cn(\"h-px bg-(--bevel-shade-strong)\", i % 5 === 0 ? \"w-full\" : \"mx-2\")} />\n        ))}\n      </span>\n      <span\n        ref={track}\n        aria-hidden\n        className=\"absolute inset-y-[calc(var(--motorized-fader-cap)/2)] w-1.5 rounded-full bg-(--bevel-material-recess) shadow-(--bevel-depth-recessed)\"\n      />\n      {/* The cap: moved with translate. The motor is slow; your hand isn't. */}\n      <span\n        aria-hidden\n        data-slot=\"motorized-fader-cap\"\n        className={cn(\n          \"bevel-metal absolute bottom-0 flex h-(--motorized-fader-cap) w-11 items-center justify-center rounded-(--bevel-radius-xs) shadow-(--bevel-depth-object)\",\n          \"transition-[translate] duration-(--bevel-duration-press) ease-(--bevel-ease-out)\",\n          \"group-data-remote:duration-(--bevel-duration-ambient) group-data-remote:ease-(--bevel-ease-in-out)\",\n          \"group-data-dragging:duration-0 motion-reduce:transition-none\",\n          \"group-focus-visible:ring-3 group-focus-visible:ring-ring/60\"\n        )}\n        style={{ translate: `0 calc(var(--motorized-fader-travel) * ${-t})` }}\n      >\n        <span className=\"h-0.5 w-full bg-(--motorized-fader-line)\" />\n      </span>\n    </div>\n  )\n}\n\ntype MotorizedFaderReadoutProps = React.ComponentProps<\"p\"> & {\n  value: string\n  changedBy?: string\n  you?: string\n}\n\n/** A scribble strip: the value, and who set it. A lamp lights when it wasn't you. */\nfunction MotorizedFaderReadout({ className, value, changedBy, you = \"You\", ...props }: MotorizedFaderReadoutProps) {\n  const remote = changedBy !== undefined && changedBy !== you\n  return (\n    <p\n      data-slot=\"motorized-fader-readout\"\n      data-remote={remote ? \"\" : undefined}\n      className={cn(\"flex flex-col items-center gap-0.5 text-center text-xs\", className)}\n      {...props}\n    >\n      <span className=\"font-mono text-sm font-semibold tabular-nums\">{value}</span>\n      {changedBy && (\n        <span className=\"flex items-center gap-1 text-muted-foreground\">\n          <span\n            aria-hidden\n            className={cn(\"size-1.5 rounded-full\", remote ? \"bevel-led [--bevel-led:var(--motorized-fader-remote)]\" : \"bg-(--bevel-shade-strong)\")}\n          />\n          {changedBy}\n        </span>\n      )}\n    </p>\n  )\n}\n\nexport { MotorizedFader, MotorizedFaderReadout }\nexport type { MotorizedFaderProps, MotorizedFaderReadoutProps }\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    ":root": {
      "--motorized-fader-travel": "9rem",
      "--motorized-fader-cap": "1.75rem",
      "--motorized-fader-panel": "oklch(0.3 0.01 260)",
      "--motorized-fader-panel-edge": "oklch(0.2 0.01 260)",
      "--motorized-fader-cap-colour": "oklch(0.86 0.01 260)",
      "--motorized-fader-cap-edge": "oklch(0.6 0.01 260)",
      "--motorized-fader-line": "oklch(0.3 0.01 260)",
      "--motorized-fader-remote": "oklch(0.78 0.15 230)"
    },
    "[data-slot=\"motorized-fader\"]": {
      "--bevel-material-recess": "oklch(0.12 0.01 260)",
      "--bevel-led": "var(--motorized-fader-remote)"
    },
    "[data-slot=\"motorized-fader-cap\"]": {
      "--bevel-material": "var(--motorized-fader-cap-colour)",
      "--bevel-material-edge": "var(--motorized-fader-cap-edge)"
    }
  },
  "categories": [
    "instruments"
  ],
  "type": "registry:ui"
}