{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "angle-slider",
  "title": "Angle Slider",
  "description": "A shadcn-style angle slider component built with Ark UI primitives.",
  "dependencies": ["@ark-ui/react", "class-variance-authority"],
  "files": [
    {
      "path": "src/components/ui/angle-slider.tsx",
      "content": "\"use client\";\n\nimport {\n  AngleSliderControl,\n  AngleSliderHiddenInput,\n  AngleSliderLabel,\n  AngleSliderMarker,\n  AngleSliderMarkerGroup,\n  AngleSliderRoot,\n  AngleSliderThumb,\n  AngleSliderValueText,\n  useAngleSliderContext,\n} from \"@ark-ui/react/angle-slider\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentProps, CSSProperties, ReactNode } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst MARKERS_45 = [0, 45, 90, 135, 180, 225, 270, 315] as const;\n\n/** Default outer diameter (`size` prop) in px. */\nconst ANGLE_SLIDER_DEFAULT_SIZE = 200;\n\n/** Smallest allowed `size` in px. */\nconst ANGLE_SLIDER_MIN_SIZE = 100;\n\n/**\n * ViewBox 200×200. Stroke 14. Centerline radius 93 → outer stroke at 100 (flush with edge).\n */\nconst RING_VIEWBOX = 200;\nconst RING_STROKE = 14;\nconst RING_TRACK_CENTERLINE_R = RING_VIEWBOX / 2 - RING_STROKE / 2;\n\n/** Distance from center to widget circle edge / outer stroke (half of square control). */\nconst EDGE_R_FR = 0.5;\n\nconst angleSliderVariants = cva(\n  \"relative flex flex-col items-center gap-2 outline-none data-disabled:pointer-events-none data-disabled:opacity-50\",\n  {\n    defaultVariants: {\n      variant: \"ring\",\n    },\n    variants: {\n      variant: {\n        labeled: \"\",\n        ring: \"\",\n        rotation: \"\",\n        split: \"\",\n        ticks: \"\",\n      },\n    },\n  },\n);\n\nconst controlVariants = cva(\n  \"relative box-border size-(--control-size) shrink-0 touch-none select-none rounded-full bg-card ring-1 ring-border data-disabled:pointer-events-none\",\n  {\n    defaultVariants: {\n      variant: \"ring\",\n    },\n    variants: {\n      variant: {\n        labeled:\n          \"shadow-[inset_0_1px_0_color-mix(in_oklab,var(--foreground)_6%,transparent)]\",\n        ring: \"\",\n        rotation:\n          \"bg-muted/40 shadow-[inset_0_2px_12px_color-mix(in_oklab,var(--background)_55%,transparent)]\",\n        split: \"\",\n        ticks: \"\",\n      },\n    },\n  },\n);\n\nexport type AngleSliderProps = Omit<\n  ComponentProps<typeof AngleSliderRoot>,\n  \"style\"\n> &\n  VariantProps<typeof angleSliderVariants> & {\n    footer?: ReactNode;\n    /** Outer width/height in px. Default {@link ANGLE_SLIDER_DEFAULT_SIZE}. Clamped to at least {@link ANGLE_SLIDER_MIN_SIZE}. */\n    size?: number;\n    style?: ComponentProps<typeof AngleSliderRoot>[\"style\"];\n  };\n\nconst knobOffsetStyle = (thumbHalf: string): CSSProperties => ({\n  transform: `translate(-50%, calc(-50% - (var(--control-size) * ${EDGE_R_FR} - ${thumbHalf})))`,\n});\n\nconst RingArc = () => {\n  const { value } = useAngleSliderContext();\n  const c = 2 * Math.PI * RING_TRACK_CENTERLINE_R;\n  const offset = c * (1 - value / 360);\n  const cx = RING_VIEWBOX / 2;\n  return (\n    <svg\n      aria-hidden\n      className=\"pointer-events-none absolute inset-0 size-full\"\n      viewBox={`0 0 ${RING_VIEWBOX} ${RING_VIEWBOX}`}\n    >\n      <circle\n        className=\"text-muted\"\n        cx={cx}\n        cy={cx}\n        fill=\"none\"\n        r={RING_TRACK_CENTERLINE_R}\n        stroke=\"currentColor\"\n        strokeWidth={RING_STROKE}\n      />\n      <circle\n        className=\"text-primary\"\n        cx={cx}\n        cy={cx}\n        fill=\"none\"\n        opacity={0.92}\n        r={RING_TRACK_CENTERLINE_R}\n        stroke=\"currentColor\"\n        strokeDasharray={c}\n        strokeDashoffset={offset}\n        strokeLinecap=\"round\"\n        strokeWidth={RING_STROKE}\n        style={{ transform: \"rotate(-90deg)\", transformOrigin: \"50% 50%\" }}\n      />\n    </svg>\n  );\n};\n\nconst RadialIndicatorLine = ({\n  emphasis,\n}: {\n  emphasis: \"primary\" | \"ring\";\n}) => (\n  <div\n    aria-hidden\n    className={cn(\n      \"pointer-events-none absolute top-1/2 left-1/2 w-px origin-bottom -translate-x-1/2 -translate-y-full\",\n      emphasis === \"primary\" &&\n        \"bg-linear-to-t from-muted-foreground via-muted-foreground to-primary\",\n      emphasis === \"ring\" && \"bg-linear-to-t from-ring/30 via-ring/80 to-ring\",\n    )}\n    style={\n      {\n        height: `calc(var(--control-size) * ${EDGE_R_FR} - 0.125rem)`,\n        rotate: \"var(--angle)\",\n      } as CSSProperties\n    }\n  />\n);\n\nconst DegreeLabels = () => (\n  <div aria-hidden className=\"pointer-events-none absolute inset-0\">\n    {MARKERS_45.map((deg) => (\n      <span\n        className=\"absolute top-1/2 left-1/2 origin-center text-[0.65rem] font-medium text-foreground\"\n        key={deg}\n        style={{\n          transform: `rotate(${deg}deg) translateY(calc(var(--control-size) * -0.56)) rotate(${-deg}deg)`,\n        }}\n      >\n        {deg}°\n      </span>\n    ))}\n  </div>\n);\n\nconst CenterDegreeReadout = ({\n  valueClassName,\n  labelClassName,\n}: {\n  labelClassName?: string;\n  valueClassName?: string;\n}) => {\n  const { value } = useAngleSliderContext();\n  return (\n    <div className=\"pointer-events-none absolute inset-0 z-0 flex flex-col items-center justify-center gap-0.5\">\n      <span\n        className={cn(\n          \"font-semibold text-[1.5rem] leading-none tabular-nums\",\n          valueClassName,\n        )}\n      >\n        {value}°\n      </span>\n      <span\n        className={cn(\"text-[0.7rem] text-muted-foreground\", labelClassName)}\n      >\n        degrees\n      </span>\n    </div>\n  );\n};\n\nconst AngleSlider = ({\n  className,\n  variant = \"ring\",\n  size = ANGLE_SLIDER_DEFAULT_SIZE,\n  footer,\n  style,\n  ...props\n}: AngleSliderProps) => {\n  const v = variant ?? \"ring\";\n  const outerPx = Math.max(ANGLE_SLIDER_MIN_SIZE, size);\n\n  const cssVars = {\n    \"--control-size\": `${outerPx}px`,\n  } as CSSProperties;\n\n  const thumbHalf = \"0.375rem\";\n\n  return (\n    <AngleSliderRoot\n      className={cn(angleSliderVariants({ variant: v }), className)}\n      style={{ ...cssVars, ...style }}\n      {...props}\n    >\n      {v === \"rotation\" && (\n        <AngleSliderLabel className=\"font-medium text-[0.65rem] text-muted-foreground tracking-[0.2em]\">\n          ROTATION\n        </AngleSliderLabel>\n      )}\n\n      <AngleSliderControl className={controlVariants({ variant: v })}>\n        {v === \"ring\" && <RingArc />}\n\n        {v === \"labeled\" && <DegreeLabels />}\n\n        {(v === \"ticks\" || v === \"split\" || v === \"labeled\") && (\n          <AngleSliderMarkerGroup className=\"absolute inset-0 z-1\">\n            {MARKERS_45.map((m) => (\n              <AngleSliderMarker\n                className={cn(\n                  \"absolute inset-0 flex items-start justify-center pt-0 text-[length:0] before:block before:h-[calc(var(--control-size)*0.1)] before:w-px before:rounded-full before:bg-current\",\n                  \"text-muted-foreground/40\",\n                  v !== \"split\" &&\n                    \"data-[state=at-value]:text-primary data-[state=at-value]:before:bg-primary data-[state=at-value]:before:shadow-[0_0_8px_color-mix(in_oklab,var(--primary)_45%,transparent)]\",\n                  v === \"split\" &&\n                    \"data-[state=at-value]:data-[value='0']:text-chart-1 data-[state=at-value]:data-[value='0']:before:bg-chart-1 data-[state=at-value]:not-data-[value='0']:text-primary data-[state=at-value]:not-data-[value='0']:before:bg-primary data-[state=at-value]:not-data-[value='0']:before:shadow-[0_0_8px_color-mix(in_oklab,var(--primary)_45%,transparent)] data-[value='0']:not-data-[state=at-value]:text-chart-1/80 data-[value='0']:not-data-[state=at-value]:before:bg-chart-1/80\",\n                )}\n                key={m}\n                value={m}\n              />\n            ))}\n          </AngleSliderMarkerGroup>\n        )}\n\n        {v === \"rotation\" && (\n          <>\n            <AngleSliderMarkerGroup className=\"absolute inset-0 z-1 opacity-60\">\n              {MARKERS_45.map((m) => (\n                <AngleSliderMarker\n                  className=\"absolute inset-0 flex items-start justify-center pt-0 text-[length:0] before:block before:h-[calc(var(--control-size)*0.09)] before:w-px before:bg-muted-foreground/50\"\n                  key={m}\n                  value={m}\n                />\n              ))}\n            </AngleSliderMarkerGroup>\n            <div\n              aria-hidden\n              className=\"pointer-events-none absolute top-1/2 left-1/2 z-1 size-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-muted-foreground\"\n            />\n            <RadialIndicatorLine emphasis=\"primary\" />\n          </>\n        )}\n\n        {(v === \"labeled\" || v === \"ticks\" || v === \"split\") && (\n          <RadialIndicatorLine emphasis=\"ring\" />\n        )}\n\n        {v === \"ring\" && (\n          <CenterDegreeReadout valueClassName=\"text-[1.65rem] text-primary\" />\n        )}\n\n        {(v === \"ticks\" || v === \"split\" || v === \"labeled\") && (\n          <CenterDegreeReadout valueClassName=\"text-foreground\" />\n        )}\n\n        <AngleSliderThumb\n          className={cn(\n            \"absolute inset-0 z-10 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          )}\n        >\n          <span\n            className={cn(\n              \"pointer-events-none absolute top-1/2 left-1/2 shrink-0 rounded-full shadow-sm ring-2 ring-ring/25\",\n              v === \"ring\" && \"size-[1.05rem] bg-primary ring-primary/35\",\n              v === \"rotation\" && \"size-3 bg-primary ring-primary/40\",\n              (v === \"ticks\" || v === \"labeled\" || v === \"split\") &&\n                \"size-3 bg-ring ring-ring/50\",\n            )}\n            style={knobOffsetStyle(thumbHalf)}\n          />\n        </AngleSliderThumb>\n      </AngleSliderControl>\n\n      {v === \"rotation\" && (\n        <AngleSliderValueText className=\"font-semibold text-foreground text-lg tabular-nums\" />\n      )}\n\n      {footer}\n\n      <AngleSliderHiddenInput />\n    </AngleSliderRoot>\n  );\n};\n\nexport {\n  ANGLE_SLIDER_DEFAULT_SIZE,\n  ANGLE_SLIDER_MIN_SIZE,\n  AngleSlider,\n  AngleSliderControl,\n  AngleSliderHiddenInput,\n  AngleSliderLabel,\n  AngleSliderMarker,\n  AngleSliderMarkerGroup,\n  AngleSliderRoot,\n  AngleSliderThumb,\n  AngleSliderValueText,\n  angleSliderVariants,\n  controlVariants,\n  MARKERS_45,\n  useAngleSliderContext,\n};\n",
      "type": "registry:ui",
      "target": "src/components/ui/angle-slider.tsx"
    }
  ],
  "type": "registry:ui"
}
