{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dial",
  "title": "Gauge and dial",
  "description": "A needle on a brass-bezelled scale. Gauge reads a value; Dial sets one, turned by drag or keyboard.",
  "dependencies": [
    "@base-ui/react",
    "cn"
  ],
  "registryDependencies": [
    "https://bevel-ui.vercel.app/r/bevel.json",
    "https://bevel-ui.vercel.app/r/amount.json"
  ],
  "files": [
    {
      "path": "registry/bevel/ui/dial.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Meter as MeterPrimitive } from \"@base-ui/react/meter\"\nimport { cn } from \"cn\"\nimport { formatAmount } from \"@/lib/format-amount\"\n\n/* The needle sweeps 270°, from bottom-left (-135°) to bottom-right (135°). */\nconst SWEEP = 270\nconst START = -SWEEP / 2\n\ntype Scale = { min: number; max: number }\nconst ScaleContext = React.createContext<Scale & { value: number }>({ min: 0, max: 100, value: 0 })\n\nfunction angleFor(value: number, { min, max }: Scale) {\n  const t = max === min ? 0 : (Math.min(Math.max(value, min), max) - min) / (max - min)\n  return START + t * SWEEP\n}\n\n/** Ticks, marks, needle and glass. Shared by Gauge and Dial. */\nfunction Face({\n  ticks,\n  dragging,\n  children,\n}: {\n  ticks: number\n  dragging?: boolean\n  children?: React.ReactNode\n}) {\n  const { value, ...scale } = React.useContext(ScaleContext)\n  return (\n    <span\n      data-slot=\"dial-face\"\n      className=\"bevel-metal relative block aspect-square w-full rounded-full p-(--bevel-stitch-inset) shadow-(--bevel-depth-object)\"\n    >\n      <span className=\"relative block size-full overflow-hidden rounded-full bg-(--bevel-ivory) text-(--bevel-ivory-foreground) shadow-(--bevel-depth-recessed)\">\n        <svg viewBox=\"0 0 100 100\" aria-hidden className=\"absolute inset-0 size-full\">\n          {Array.from({ length: ticks + 1 }, (_, i) => {\n            const major = i % 5 === 0 || i === ticks\n            return (\n              <line\n                key={i}\n                x1=\"50\"\n                y1={major ? 9 : 11}\n                x2=\"50\"\n                y2=\"16\"\n                stroke=\"currentColor\"\n                strokeWidth={major ? 1.6 : 0.8}\n                transform={`rotate(${START + (i / ticks) * SWEEP} 50 50)`}\n              />\n            )\n          })}\n        </svg>\n        {children}\n        <span\n          aria-hidden\n          data-slot=\"dial-needle\"\n          data-dragging={dragging ? \"\" : undefined}\n          className=\"absolute inset-0 transition-[rotate] duration-(--bevel-duration-ambient) ease-(--bevel-ease-spring) data-dragging:duration-0 motion-reduce:transition-none\"\n          style={{ rotate: `${angleFor(value, scale)}deg` }}\n        >\n          <svg viewBox=\"0 0 100 100\" className=\"size-full drop-shadow-(--bevel-depth-cast-sm)\">\n            <path d=\"M50 12 L52 50 L48 50 Z\" fill=\"var(--bevel-needle)\" />\n            <path d=\"M48 50 L52 50 L51 60 L49 60 Z\" fill=\"currentColor\" />\n          </svg>\n        </span>\n        <span\n          aria-hidden\n          className=\"bevel-metal absolute top-1/2 left-1/2 size-[12%] -translate-1/2 rounded-full\"\n        />\n        <span aria-hidden className=\"bevel-glass absolute inset-0 rounded-full\" />\n      </span>\n    </span>\n  )\n}\n\ntype DialMarkProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  value: number\n  tone?: \"red\" | \"amber\" | \"green\"\n}\n\n/** A painted mark on the scale: a threshold, a limit, a target. Decorative; describe it in text too. */\nfunction DialMark({ value, tone = \"red\", className, style, ...props }: DialMarkProps) {\n  const scale = React.useContext(ScaleContext)\n  return (\n    <span\n      aria-hidden\n      data-slot=\"dial-mark\"\n      data-tone={tone}\n      className={cn(\"absolute inset-0\", className)}\n      style={{ rotate: `${angleFor(value, scale)}deg`, ...style }}\n      {...props}\n    >\n      <svg viewBox=\"0 0 100 100\" className=\"size-full\">\n        <path d=\"M48.2 6 L51.8 6 L51 18 L49 18 Z\" fill=\"var(--dial-mark)\" />\n      </svg>\n    </span>\n  )\n}\n\ntype GaugeProps = MeterPrimitive.Root.Props & {\n  /** Number of tick intervals on the scale. */\n  ticks?: number\n  /** Marks (DialMark) painted on the face. */\n  marks?: React.ReactNode\n}\n\n/** A read-only instrument: the needle shows a value on a scale. Announced as a meter. */\nfunction Gauge({\n  className,\n  value,\n  min = 0,\n  max = 100,\n  ticks = 20,\n  marks,\n  children,\n  ...props\n}: GaugeProps) {\n  return (\n    <ScaleContext value={{ min, max, value }}>\n      <MeterPrimitive.Root\n        data-slot=\"gauge\"\n        value={value}\n        min={min}\n        max={max}\n        className={cn(\"flex w-48 flex-col items-center gap-3 text-center\", className)}\n        {...props}\n      >\n        <Face ticks={ticks}>{marks}</Face>\n        {children}\n      </MeterPrimitive.Root>\n    </ScaleContext>\n  )\n}\n\nfunction GaugeLabel({ className, ...props }: MeterPrimitive.Label.Props) {\n  return (\n    <MeterPrimitive.Label\n      data-slot=\"gauge-label\"\n      className={cn(\"text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction GaugeValue({ className, ...props }: MeterPrimitive.Value.Props) {\n  return (\n    <MeterPrimitive.Value\n      data-slot=\"gauge-value\"\n      className={cn(\"text-lg font-semibold tabular-nums\", className)}\n      {...props}\n    />\n  )\n}\n\ntype DialProps = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\" | \"children\"> & {\n  value?: number\n  defaultValue?: number\n  onValueChange?: (value: number) => void\n  min?: number\n  max?: number\n  step?: number\n  /** Number of tick intervals on the scale. */\n  ticks?: number\n  /** Formats the value for the readout and for screen readers. */\n  currency?: string\n  format?: Intl.NumberFormatOptions\n  locale?: Intl.LocalesArgument\n  /** Marks (DialMark) painted on the face. */\n  marks?: React.ReactNode\n  /** A name for the control, shown below the dial. */\n  label: React.ReactNode\n  disabled?: boolean\n}\n\n/**\n * An adjustable dial: turn it with a drag, or arrow keys, Page Up/Down, Home and End.\n * Announced as a slider.\n */\nfunction Dial({\n  className,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  min = 0,\n  max = 100,\n  step = 1,\n  ticks = 20,\n  currency,\n  format,\n  locale,\n  marks,\n  label,\n  disabled,\n  ...props\n}: DialProps) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultValue ?? min)\n  const value = valueProp ?? uncontrolled\n  const [dragging, setDragging] = React.useState(false)\n  const labelId = React.useId()\n  const text = formatAmount({ value, currency, format, locale })\n\n  const commit = (next: number) => {\n    const snapped = Math.min(max, Math.max(min, Math.round((next - min) / step) * step + min))\n    if (snapped === value) return\n    if (valueProp === undefined) setUncontrolled(snapped)\n    onValueChange?.(snapped)\n  }\n\n  const fromPointer = (event: React.PointerEvent<HTMLDivElement>) => {\n    const rect = event.currentTarget.getBoundingClientRect()\n    const dx = event.clientX - (rect.left + rect.width / 2)\n    const dy = event.clientY - (rect.top + rect.height / 2)\n    // 0° points up; clockwise is positive. Below the scale, snap to the nearer end.\n    let angle = (Math.atan2(dx, -dy) * 180) / Math.PI\n    if (angle > SWEEP / 2) angle = SWEEP / 2\n    if (angle < -SWEEP / 2) angle = -SWEEP / 2\n    commit(min + ((angle - START) / SWEEP) * (max - min))\n  }\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    const big = Math.max(step, (max - min) / 10)\n    const next = {\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    }[event.key]\n    if (next === undefined) return\n    event.preventDefault()\n    commit(next)\n  }\n\n  return (\n    <ScaleContext value={{ min, max, value }}>\n      <div\n        data-slot=\"dial\"\n        data-disabled={disabled ? \"\" : undefined}\n        className={cn(\n          \"flex w-48 flex-col items-center gap-3 text-center data-disabled:opacity-60\",\n          className\n        )}\n        {...props}\n      >\n        <div\n          role=\"slider\"\n          tabIndex={disabled ? -1 : 0}\n          aria-labelledby={labelId}\n          aria-valuemin={min}\n          aria-valuemax={max}\n          aria-valuenow={value}\n          aria-valuetext={text}\n          aria-disabled={disabled || undefined}\n          data-slot=\"dial-control\"\n          className=\"w-full cursor-grab touch-none rounded-full outline-none select-none focus-visible:ring-3 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-background active:cursor-grabbing aria-disabled:cursor-not-allowed\"\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 (dragging) fromPointer(event)\n          }}\n          onPointerUp={() => setDragging(false)}\n          onPointerCancel={() => setDragging(false)}\n        >\n          <Face ticks={ticks} dragging={dragging}>\n            {marks}\n          </Face>\n        </div>\n        <span id={labelId} data-slot=\"dial-label\" className=\"text-sm text-muted-foreground\">\n          {label}\n        </span>\n        <span aria-hidden data-slot=\"dial-value\" className=\"text-lg font-semibold tabular-nums\">\n          {text}\n        </span>\n      </div>\n    </ScaleContext>\n  )\n}\n\nexport { Gauge, GaugeLabel, GaugeValue, Dial, DialMark }\nexport type { GaugeProps, DialProps, DialMarkProps }\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    ":root": {
      "--dial-mark-red": "oklch(0.58 0.2 27)",
      "--dial-mark-amber": "oklch(0.75 0.16 70)",
      "--dial-mark-green": "oklch(0.6 0.15 150)"
    },
    "[data-slot=\"gauge\"], [data-slot=\"dial\"]": {
      "--bevel-material": "var(--bevel-brass)",
      "--bevel-material-edge": "var(--bevel-brass-edge)"
    },
    "[data-slot=\"dial-mark\"]": {
      "--dial-mark": "var(--dial-mark-red)"
    },
    "[data-slot=\"dial-mark\"][data-tone=\"amber\"]": {
      "--dial-mark": "var(--dial-mark-amber)"
    },
    "[data-slot=\"dial-mark\"][data-tone=\"green\"]": {
      "--dial-mark": "var(--dial-mark-green)"
    }
  },
  "categories": [
    "instruments"
  ],
  "type": "registry:ui"
}