{
  "$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://mcp.xzero.co.uk/r/xzero-motion-tokens.json"
  ],
  "files": [
    {
      "path": "registry/components/switch/switch.tsx",
      "type": "registry:component",
      "target": "@components/xzero/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<typeof SwitchPrimitive.Root> {\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<ElementRef<typeof SwitchPrimitive.Root>, 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    <SwitchPrimitive.Root\n      {...props}\n      ref={ref}\n      checked={on}\n      onCheckedChange={next => { 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      <span className={styles.track}>\n        {/* The thumb stretches like a held finger and keeps its far edge anchored, then travels on a spring. */}\n        <motion.span className={styles.thumb} style={{ scaleX }} initial={false} animate={{ x: on ? travel - extra : 0, width: size + extra }} transition={reduceMotion ? { duration: 0 } : { x: glide, width: motionTokens.spring.snappy }} />\n      </span>\n      {label ? <span className={styles.label}>{label}</span> : null}\n    </SwitchPrimitive.Root>\n  );\n});\n\nSwitch.displayName = \"Switch\";\n\nexport default Switch;\n"
    },
    {
      "path": "registry/components/switch/switch.module.css",
      "type": "registry:component",
      "target": "@components/xzero/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://ui.xzero.co.uk/components/switch",
  "meta": {
    "tier": "free",
    "kind": "component",
    "tags": [
      "toggle",
      "settings"
    ],
    "docs": "https://ui.xzero.co.uk/components/switch",
    "markdown": "https://ui.xzero.co.uk/components/switch/markdown"
  }
}