{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "angle-slider",
  "title": "Angle Slider",
  "description": "A circular handle for choosing an angle from 0 to 359 degrees, with wrap-around, markers and snapping, for gradients, shadows and rotation.",
  "files": [
    {
      "path": "registry/tealmac/angle-slider.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface AngleSliderProps extends Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onChange\" | \"defaultValue\"\n> {\n  /** Degrees, 0 at the top, increasing clockwise. */\n  value?: number\n  defaultValue?: number\n  onValueChange?: (degrees: number) => void\n  /** Snap size in degrees. Default 1. */\n  step?: number\n  /** Diameter in px. Default 128. */\n  size?: number\n  /** Names the control, for example \"Light direction\". */\n  label: string\n  /** Show tick marks at 0, 90, 180 and 270. Default true. */\n  markers?: boolean\n  disabled?: boolean\n}\n\nconst norm = (d: number) => ((d % 360) + 360) % 360\n\n/**\n * A circular handle for choosing an angle from 0 to 359 degrees: a gradient\n * direction, a shadow angle, a rotation. Drag the handle around the ring, or\n * use the arrow keys (1 step), Shift plus arrows (15 degrees), Page Up and Down\n * (45 degrees) and Home for 0. The value wraps from 359 to 0. Click the ring\n * to jump to that angle.\n */\nexport function AngleSlider({\n  value: valueProp,\n  defaultValue = 0,\n  onValueChange,\n  step = 1,\n  size = 128,\n  label,\n  markers = true,\n  disabled = false,\n  className,\n  ...props\n}: AngleSliderProps) {\n  const [inner, setInner] = React.useState(defaultValue)\n  const value = norm(valueProp ?? inner)\n  const ref = React.useRef<HTMLDivElement>(null)\n\n  const commit = (deg: number) => {\n    const next = norm(Math.round(deg / step) * step)\n    if (next === value) return\n    if (valueProp === undefined) setInner(next)\n    onValueChange?.(next)\n  }\n\n  const fromPoint = (clientX: number, clientY: number) => {\n    const rect = ref.current!.getBoundingClientRect()\n    const dx = clientX - (rect.left + rect.width / 2)\n    const dy = clientY - (rect.top + rect.height / 2)\n    return norm((Math.atan2(dx, -dy) * 180) / Math.PI)\n  }\n\n  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (disabled) return\n    e.preventDefault()\n    const el = e.currentTarget\n    el.focus()\n    el.setPointerCapture(e.pointerId)\n    commit(fromPoint(e.clientX, e.clientY))\n    const move = (ev: PointerEvent) => commit(fromPoint(ev.clientX, ev.clientY))\n    const up = () => {\n      el.removeEventListener(\"pointermove\", move)\n      el.removeEventListener(\"pointerup\", up)\n      el.removeEventListener(\"pointercancel\", up)\n    }\n    el.addEventListener(\"pointermove\", move)\n    el.addEventListener(\"pointerup\", up)\n    el.addEventListener(\"pointercancel\", up)\n  }\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    if (disabled) return\n    const big = e.shiftKey ? 15 : step\n    const keys: Record<string, number> = {\n      ArrowRight: value + big,\n      ArrowUp: value + big,\n      ArrowLeft: value - big,\n      ArrowDown: value - big,\n      PageUp: value + 45,\n      PageDown: value - 45,\n      Home: 0,\n      End: 360 - step,\n    }\n    if (!(e.key in keys)) return\n    e.preventDefault()\n    commit(keys[e.key])\n  }\n\n  const c = size / 2\n  const rad = ((value - 90) * Math.PI) / 180\n  const hx = c + (c - 14) * Math.cos(rad)\n  const hy = c + (c - 14) * Math.sin(rad)\n  return (\n    <div\n      ref={ref}\n      role=\"slider\"\n      tabIndex={disabled ? -1 : 0}\n      aria-label={label}\n      aria-valuemin={0}\n      aria-valuemax={359}\n      aria-valuenow={Math.round(value)}\n      aria-valuetext={`${Math.round(value)} degrees`}\n      aria-disabled={disabled || undefined}\n      onPointerDown={onPointerDown}\n      onKeyDown={onKeyDown}\n      className={cn(\n        \"focus-visible:ring-ring/50 relative inline-block cursor-pointer touch-none rounded-full outline-none select-none focus-visible:ring-4\",\n        disabled && \"cursor-not-allowed opacity-50\",\n        className\n      )}\n      style={{ width: size, height: size }}\n      {...props}\n    >\n      <svg width={size} height={size} aria-hidden>\n        <circle\n          cx={c}\n          cy={c}\n          r={c - 14}\n          fill=\"none\"\n          strokeWidth={2}\n          className=\"stroke-border\"\n        />\n        {markers\n          ? [0, 90, 180, 270].map((d) => {\n              const r = ((d - 90) * Math.PI) / 180\n              return (\n                <line\n                  key={d}\n                  x1={c + (c - 22) * Math.cos(r)}\n                  y1={c + (c - 22) * Math.sin(r)}\n                  x2={c + (c - 6) * Math.cos(r)}\n                  y2={c + (c - 6) * Math.sin(r)}\n                  strokeWidth={2}\n                  className=\"stroke-muted-foreground/50\"\n                />\n              )\n            })\n          : null}\n        <line\n          x1={c}\n          y1={c}\n          x2={hx}\n          y2={hy}\n          strokeWidth={2}\n          className=\"stroke-primary\"\n        />\n        <circle cx={c} cy={c} r={3} className=\"fill-primary\" />\n        <circle\n          cx={hx}\n          cy={hy}\n          r={9}\n          className=\"fill-primary stroke-background\"\n          strokeWidth={2}\n        />\n      </svg>\n      <span className=\"pointer-events-none absolute inset-0 flex items-center justify-center text-sm font-medium tabular-nums\">\n        <span className=\"bg-background/70 rounded px-1\">\n          {Math.round(value)}&deg;\n        </span>\n      </span>\n    </div>\n  )\n}\n\nexport default AngleSlider\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}