{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"switch","type":"registry:ui","title":"Switch","description":"A tactile toggle for settings that take effect immediately.","categories":["inputs"],"dependencies":["@radix-ui/react-switch","motion"],"registryDependencies":["https://uiarc.dev/r/arc-foundation.json"],"files":[{"path":"registry/components/switch/switch.tsx","type":"registry:component","target":"@components/arc/switch/switch.tsx","content":"\"use client\";\n\nimport { forwardRef, useEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ElementRef } from \"react\";\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\";\nimport { animate, motion, useMotionValue, useReducedMotion } from \"motion/react\";\nimport type { Transition } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport styles from \"./switch.module.css\";\n\nexport interface SwitchProps extends ComponentPropsWithoutRef {\n label?: string;\n}\n\n/** Track inner width (42 - 6 padding) minus the 18px thumb; keep in sync with switch.module.css. */\nconst size = 18;\nconst travel = 18;\n/** How far the thumb widens toward the other side while pressed. */\nconst stretch = 5;\n/** Critically damped: the thumb lands on its end without overshooting the state it reports. */\nconst glide: Transition = { type: \"spring\", visualDuration: 0.3, bounce: 0 };\n\nexport const Switch = forwardRef, SwitchProps>(function Switch(\n { label, className, checked, defaultChecked, onCheckedChange, onPointerDown, onPointerUp, onPointerLeave, onPointerCancel, onKeyDown, onKeyUp, onBlur, ...props },\n ref,\n) {\n const reduceMotion = useReducedMotion();\n const [internal, setInternal] = useState(defaultChecked ?? false);\n const [pressed, setPressed] = useState(false);\n const on = checked ?? internal;\n // A brief stretch along the travel, so the thumb reads as moving mass rather than a sliding dot.\n const scaleX = useMotionValue(1);\n const shown = useRef(on);\n // A pointer or Space press already stretched the thumb, so its release should not add a second stretch on top.\n const releasedAt = useRef(-Infinity);\n useEffect(() => {\n if (shown.current === on) return;\n shown.current = on;\n const fromPress = performance.now() - releasedAt.current < 250;\n if (reduceMotion || fromPress) return;\n const controls = animate(scaleX, [1, 1.16, 1], { duration: 0.34, times: [0, 0.4, 1], ease: [\"easeOut\", \"easeInOut\"] });\n return () => controls.stop();\n }, [on, reduceMotion, scaleX]);\n const extra = pressed && !reduceMotion && !props.disabled ? stretch : 0;\n const classes = [styles.switch, className].filter(Boolean).join(\" \");\n\n return (\n { if (checked === undefined) setInternal(next); onCheckedChange?.(next); }}\n onPointerDown={event => { onPointerDown?.(event); if (event.button === 0) setPressed(true); }}\n onPointerUp={event => { onPointerUp?.(event); if (pressed && !props.disabled) releasedAt.current = performance.now(); setPressed(false); }}\n onPointerLeave={event => { onPointerLeave?.(event); setPressed(false); }}\n onPointerCancel={event => { onPointerCancel?.(event); setPressed(false); }}\n onKeyDown={event => { onKeyDown?.(event); if (event.key === \" \") setPressed(true); }}\n onKeyUp={event => { onKeyUp?.(event); if (pressed && !props.disabled) releasedAt.current = performance.now(); setPressed(false); }}\n onBlur={event => { onBlur?.(event); setPressed(false); }}\n className={classes}\n aria-label={props[\"aria-label\"] ?? label}\n >\n \n {/* The thumb stretches like a held finger and keeps its far edge anchored, then travels on a spring. */}\n \n \n {label ? {label} : null}\n \n );\n});\n\nSwitch.displayName = \"Switch\";\n\nexport default Switch;\n"},{"path":"registry/components/switch/switch.module.css","type":"registry:component","target":"@components/arc/switch/switch.module.css","content":".switch { display: inline-flex; min-height: var(--control-height-md); align-items: center; gap: var(--space-3); padding: 0; border: 0; color: var(--foreground); background: transparent; font: inherit; font-size: var(--text-sm); cursor: pointer; -webkit-tap-highlight-color: transparent; }\n.switch:disabled { cursor: not-allowed; opacity: .5; }\n/* Fixed box: the thumb moves inside it with transforms, so nothing around the switch shifts. The track crossfades between the two fills. */\n.track { position: relative; box-sizing: border-box; display: flex; flex: 0 0 auto; width: 42px; height: 24px; align-items: center; padding: 3px; border-radius: var(--radius-pill); background: var(--control-track); isolation: isolate; transition: background-color var(--duration-fast) var(--ease-standard); }\n.track::before { content: \"\"; position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: var(--control-on); opacity: 0; transition: opacity var(--duration-standard) var(--ease-standard); }\n.switch[data-state=\"checked\"] .track::before { opacity: 1; }\n.thumb { flex: 0 0 auto; width: 18px; height: 18px; border-radius: var(--radius-pill); background: var(--control-thumb); box-shadow: var(--control-thumb-shadow); will-change: transform; transition: background-color var(--duration-standard) var(--ease-standard); }\n.switch[data-state=\"checked\"] .thumb { background: var(--control-thumb-on); }\n@media (hover: hover) and (pointer: fine) { .switch[data-state=\"unchecked\"]:hover:not(:disabled) .track { background: var(--control-track-hover); } }\n.switch:focus-visible { outline: none; }\n.label { line-height: var(--leading-body); }\n@media (prefers-reduced-motion: reduce) { .track, .track::before, .thumb { transition: none; } }\n"}],"docs":"Docs and live preview: https://uiarc.dev/components/switch","meta":{"tier":"free","kind":"component","docs":"https://uiarc.dev/components/switch","markdown":"https://uiarc.dev/components/switch/markdown","tags":["toggle","settings"]}}