{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "switch",
  "title": "Switch",
  "description": "A toggle switch with thumb travel animation and hidden checkbox for forms.",
  "dependencies": [],
  "files": [
    {
      "path": "components/ui/switch.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type HTMLAttributes,\n} from 'react'\nimport { animate, motion, useMotionValue, useReducedMotion } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { springs } from '@/lib/motion-tokens'\nimport { playHoverSound, playClickSound } from '@/lib/sound'\n\nconst TRACK_W = 44\nconst THUMB_SIZE = 20\nconst THUMB_TRAVEL = TRACK_W - THUMB_SIZE - 4\nexport interface SwitchProps extends Omit<HTMLAttributes<HTMLButtonElement>, 'onChange'> {\n  checked?: boolean\n  defaultChecked?: boolean\n  onCheckedChange?: (checked: boolean) => void\n  label?: string\n  labelSide?: 'left' | 'right'\n  name?: string\n  disabled?: boolean\n  required?: boolean\n  'aria-invalid'?: boolean\n  'aria-describedby'?: string\n}\n\nexport const Switch = forwardRef<HTMLButtonElement, SwitchProps>(\n  (\n    {\n      checked: checkedProp,\n      defaultChecked = false,\n      onCheckedChange,\n      label,\n      labelSide = 'right',\n      disabled,\n      name,\n      required,\n      className,\n      id: idProp,\n      'aria-invalid': ariaInvalid,\n      'aria-describedby': ariaDescribedby,\n      ...props\n    },\n    ref,\n  ) => {\n    const reactId = useId()\n    const id = idProp ?? reactId\n    const isControlled = checkedProp !== undefined\n    const [internal, setInternal] = useState(defaultChecked)\n    const value = isControlled ? checkedProp : internal\n    const reduceMotion = useReducedMotion()\n\n    const thumbX = useMotionValue(value ? THUMB_TRAVEL : 0)\n    const thumbScaleX = useMotionValue(1)\n\n    const prevValue = useRef(value)\n    useEffect(() => {\n      if (prevValue.current === value) return\n      prevValue.current = value\n      animate(thumbX, value ? THUMB_TRAVEL : 0, springs.settle)\n    }, [value, thumbX])\n\n    const toggle = useCallback(() => {\n      if (disabled) return\n      playClickSound()\n      const next = !value\n      if (!isControlled) setInternal(next)\n      onCheckedChange?.(next)\n    }, [value, isControlled, onCheckedChange, disabled])\n    const handlePointerDown = useCallback(() => {\n      if (reduceMotion || disabled) return\n      animate(thumbScaleX, 1.15, springs.press)\n    }, [reduceMotion, disabled, thumbScaleX])\n\n    const handlePointerUp = useCallback(() => {\n      if (reduceMotion || disabled) return\n      animate(thumbScaleX, 1, springs.settle)\n    }, [reduceMotion, disabled, thumbScaleX])\n\n    return (\n      <div\n        className={cn(\n          'flex items-center gap-3',\n          labelSide === 'left' && 'flex-row-reverse',\n          className,\n        )}\n      >\n        <input\n          type=\"checkbox\"\n          id={id}\n          name={name}\n          checked={value}\n          disabled={disabled}\n          required={required}\n          readOnly\n          className=\"sr-only\"\n          tabIndex={-1}\n        />\n        <button\n          ref={ref}\n          type=\"button\"\n          role=\"switch\"\n          aria-checked={value}\n          aria-label={label}\n          aria-invalid={ariaInvalid}\n          aria-describedby={ariaDescribedby}\n          disabled={disabled}\n          onClick={toggle}\n          onMouseEnter={() => {\n            if (!disabled) playHoverSound()\n          }}\n          className={cn(\n            'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full p-0.5 transition-colors',\n            'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-2 focus-visible:ring-offset-(--color-bg)',\n            'disabled:cursor-not-allowed disabled:opacity-50',\n            value ? 'bg-(--color-fg)' : 'bg-(--color-border)',\n            ariaInvalid && 'ring-2 ring-(--color-error) ring-offset-2 ring-offset-(--color-bg)',\n          )}\n          onPointerDown={(e) => {\n            e.preventDefault()\n            handlePointerDown()\n          }}\n          onPointerUp={handlePointerUp}\n          onPointerCancel={handlePointerUp}\n          {...props}\n        >\n          <motion.span\n            className=\"pointer-events-none inline-block size-5 rounded-full bg-(--color-bg) shadow-sm\"\n            style={{\n              x: thumbX,\n              scaleX: thumbScaleX,\n            }}\n          />\n        </button>\n        {label && (\n          <label\n            htmlFor={id}\n            className=\"min-h-6 flex items-center text-sm text-(--color-fg) select-none\"\n            onClick={() => {\n              if (!disabled) toggle()\n            }}\n          >\n            {label}\n          </label>\n        )}\n      </div>\n    )\n  },\n)\nSwitch.displayName = 'Switch'\nexport function SwitchPreview() {\n  const [checked, setChecked] = useState(false)\n  return (\n    <div className=\"flex h-full w-full items-center justify-center p-6\">\n      <div className=\"space-y-3\">\n        <Switch checked={checked} onCheckedChange={setChecked} label=\"Notifications\" />\n        <Switch defaultChecked label=\"Dark mode\" />\n        <Switch checked disabled label=\"Disabled\" />\n      </div>\n    </div>\n  )\n}\n"
    }
  ],
  "type": "registry:ui"
}
