{"slug":"switch","name":"Switch","description":"Toggle with a spring-driven thumb and press feedback.","category":"motion","source_url":"https://beui.saura3h.xyz/r/switch/raw","detail_url":"https://beui.saura3h.xyz/r/switch","raw_url":"https://beui.saura3h.xyz/r/switch/raw","page_url":"https://beui.saura3h.xyz/components/motion/switch","dependencies":["motion","react"],"internal":["@/components/motion/switch","@/lib/utils"],"files":[{"path":"components/motion/switch.tsx","type":"component","content":"\"use client\";\n\nimport { animate, motion, MotionConfig } from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface SwitchProps {\n  checked: boolean;\n  onCheckedChange: (checked: boolean) => void;\n  disabled?: boolean;\n  label?: string;\n  className?: string;\n}\n\nexport function Switch({ checked, onCheckedChange, disabled, label, className }: SwitchProps) {\n  const id = useId();\n  const thumbRef = useRef<HTMLDivElement>(null);\n  const [isPressed, setIsPressed] = useState(false);\n  const [isPointer, setIsPointer] = useState(false);\n\n  // Disabled shake feedback when pressed.\n  useEffect(() => {\n    if (!thumbRef.current) return;\n    if (disabled && isPressed) {\n      animate(\n        thumbRef.current,\n        { x: [0, -2, 2, -1, 0] },\n        { delay: 0.2, duration: 0.6 },\n      );\n    }\n  }, [disabled, isPressed]);\n\n  const squish = !disabled && isPointer && isPressed;\n\n  return (\n    <MotionConfig transition={{ type: \"spring\", stiffness: 800, damping: 80, mass: 4 }}>\n      <span className={cn(\"inline-flex items-center gap-3\", className)}>\n        <motion.button\n          id={id}\n          type=\"button\"\n          role=\"switch\"\n          aria-checked={checked}\n          disabled={disabled}\n          onClick={() => !disabled && onCheckedChange(!checked)}\n          onPointerDown={(e) => {\n            setIsPressed(true);\n            setIsPointer(e.type.startsWith(\"pointer\"));\n          }}\n          onPointerUp={() => setIsPressed(false)}\n          onPointerLeave={() => setIsPressed(false)}\n          initial={false}\n          data-state={checked ? \"checked\" : \"unchecked\"}\n          className={cn(\n            \"group peer inline-flex h-7 w-12 shrink-0 cursor-pointer items-center px-1 rounded-full outline-none transition-colors duration-200\",\n            \"focus-visible:ring-2 focus-visible:ring-(--color-border-strong) focus-visible:ring-offset-2 focus-visible:ring-offset-(--color-bg)\",\n            \"disabled:cursor-not-allowed disabled:opacity-60\",\n            checked ? \"justify-end bg-(--color-fg)\" : \"justify-start bg-(--color-fg-muted)/60\",\n          )}\n        >\n          <motion.div\n            ref={thumbRef}\n            layout\n            animate={{ scale: squish ? 0.9 : 1 }}\n            className=\"pointer-events-none block h-5 w-5 rounded-full bg-(--color-bg) shadow-[0_2px_6px_-1px_rgb(0_0_0/0.3)]\"\n          >\n            {/* Stretch toward the destination on press. */}\n            <div\n              className={cn(\n                \"size-5\",\n                squish && (checked ? \"ml-1\" : \"mr-1\"),\n              )}\n            />\n          </motion.div>\n        </motion.button>\n        {label ? (\n          <label htmlFor={id} className=\"cursor-pointer text-sm text-(--color-fg)\">\n            {label}\n          </label>\n        ) : null}\n      </span>\n    </MotionConfig>\n  );\n}\n"},{"path":"components/previews/motion/switch.preview.tsx","type":"preview","content":"\"use client\";\n\nimport { useState } from \"react\";\nimport { Switch } from \"@/components/motion/switch\";\n\nexport function SwitchPreview() {\n  const [on, setOn] = useState(true);\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <Switch checked={on} onCheckedChange={setOn} label=\"Enable notifications\" />\n      <Switch checked={false} onCheckedChange={() => {}} label=\"Off\" />\n      <Switch checked disabled onCheckedChange={() => {}} label=\"Disabled\" />\n    </div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"util","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}