{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "switch",
  "title": "Switch",
  "description": "Base UI switch with a squishy, draggable, spring-driven thumb.",
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "springs"
  ],
  "files": [
    {
      "path": "src/components/ui/switch.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { Switch as SwitchPrimitive } from \"@base-ui/react/switch\";\nimport { motion, useMotionValue, useReducedMotion, animate } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { spring } from \"@/lib/springs\";\n\nconst SIZES = {\n  default: { track: 34, height: 20, thumb: 16 },\n  sm: { track: 24, height: 14, thumb: 12 },\n} as const;\n\nconst DRAG_DEAD_ZONE = 2;\n\nfunction Switch({\n  className,\n  size = \"default\",\n  checked: checkedProp,\n  defaultChecked,\n  onCheckedChange,\n  disabled = false,\n  ...props\n}: Omit<SwitchPrimitive.Root.Props, \"inputRef\"> & {\n  size?: \"sm\" | \"default\";\n}) {\n  const { track, height, thumb } = SIZES[size];\n  const offset = (height - thumb) / 2;\n  const travel = track - thumb - offset * 2;\n  const hoverExtend = (2 * thumb) / 16;\n  const pressExtend = (4 * thumb) / 16;\n  const pressShrink = (4 * thumb) / 16;\n\n  const [uncontrolledChecked, setUncontrolledChecked] = useState(defaultChecked ?? false);\n  const checked = checkedProp ?? uncontrolledChecked;\n\n  const applyChecked = (next: boolean, eventDetails?: SwitchPrimitive.Root.ChangeEventDetails) => {\n    if (checkedProp === undefined) setUncontrolledChecked(next);\n    onCheckedChange?.(next, eventDetails as SwitchPrimitive.Root.ChangeEventDetails);\n  };\n\n  const [hovered, setHovered] = useState(false);\n  const [pressed, setPressed] = useState(false);\n  const dragging = useRef(false);\n  const suppressNextClick = useRef(false);\n  const dragStart = useRef<{ clientX: number; originX: number } | null>(null);\n\n  const thumbWidth = pressed ? thumb + pressExtend : hovered ? thumb + hoverExtend : thumb;\n  const thumbHeight = pressed ? thumb - pressShrink : thumb;\n  const thumbY = pressed ? offset + pressShrink / 2 : offset;\n  const thumbX = checked ? offset + travel - (thumbWidth - thumb) : offset;\n\n  const motionX = useMotionValue(thumbX);\n  const reduceMotion = useReducedMotion();\n  const hasMountedRef = useRef(false);\n  useEffect(() => {\n    hasMountedRef.current = true;\n  }, []);\n\n  useEffect(() => {\n    if (dragging.current) return;\n    if (!hasMountedRef.current) {\n      motionX.set(thumbX);\n      return;\n    }\n    const controls = animate(motionX, thumbX, spring.moderate.enter);\n    return () => controls.stop();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [thumbX]);\n\n  return (\n    <SwitchPrimitive.Root\n      data-slot=\"switch\"\n      data-size={size}\n      checked={checked}\n      disabled={disabled}\n      onCheckedChange={(next, eventDetails) => {\n        if (suppressNextClick.current) return;\n        applyChecked(next, eventDetails);\n      }}\n      className={cn(\n        \"group/switch relative shrink-0 touch-none rounded-full border border-transparent outline-none transition-colors after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-disabled:cursor-not-allowed data-disabled:opacity-50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40\",\n        checked\n          ? hovered\n            ? \"bg-accent-blue-hover\"\n            : \"bg-accent-blue\"\n          : hovered\n            ? \"bg-[color-mix(in_oklch,var(--input),var(--foreground)_10%)] dark:bg-[color-mix(in_oklch,var(--input),var(--foreground)_14%)]\"\n            : \"bg-input\",\n        className,\n      )}\n      style={{ width: track, height }}\n      onPointerEnter={(e) => {\n        if (!disabled && e.pointerType === \"mouse\") setHovered(true);\n      }}\n      onPointerLeave={() => setHovered(false)}\n      onPointerDown={(e) => {\n        if (disabled) return;\n        if (e.pointerType === \"mouse\" && e.button !== 0) return;\n        setPressed(true);\n        dragging.current = false;\n        suppressNextClick.current = false;\n        dragStart.current = { clientX: e.clientX, originX: motionX.get() };\n        e.currentTarget.setPointerCapture(e.pointerId);\n      }}\n      onPointerMove={(e) => {\n        if (!dragStart.current) return;\n        const delta = e.clientX - dragStart.current.clientX;\n        if (!dragging.current) {\n          if (Math.abs(delta) < DRAG_DEAD_ZONE) return;\n          dragging.current = true;\n        }\n        const pressedWidth = thumb + pressExtend;\n        const min = offset;\n        const max = track - offset - pressedWidth;\n        motionX.set(Math.max(min, Math.min(max, dragStart.current.originX + delta)));\n      }}\n      onPointerUp={() => {\n        if (!dragStart.current) return;\n        setPressed(false);\n        if (dragging.current) {\n          suppressNextClick.current = true;\n          dragging.current = false;\n          const pressedWidth = thumb + pressExtend;\n          const min = offset;\n          const max = track - offset - pressedWidth;\n          const shouldBeOn = motionX.get() > (min + max) / 2;\n          if (shouldBeOn !== checked) {\n            applyChecked(shouldBeOn);\n          } else {\n            animate(\n              motionX,\n              checked ? offset + travel - pressExtend : offset,\n              spring.moderate.enter,\n            );\n          }\n          requestAnimationFrame(() => {\n            suppressNextClick.current = false;\n          });\n        }\n        dragStart.current = null;\n      }}\n      onPointerCancel={() => {\n        if (!dragStart.current) return;\n        setPressed(false);\n        if (dragging.current) {\n          dragging.current = false;\n          animate(motionX, thumbX, spring.moderate.enter);\n        }\n        dragStart.current = null;\n      }}\n      {...props}\n    >\n      <motion.span\n        data-slot=\"switch-thumb\"\n        className=\"pointer-events-none absolute -top-px -left-px block rounded-full bg-background shadow-sm dark:bg-foreground\"\n        style={{ x: motionX }}\n        animate={{ y: thumbY, width: thumbWidth, height: thumbHeight }}\n        transition={\n          !hasMountedRef.current || reduceMotion ? { duration: 0 } : spring.moderate.enter\n        }\n      />\n    </SwitchPrimitive.Root>\n  );\n}\n\nexport { Switch };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}