{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "switch",
  "type": "registry:ui",
  "title": "Switch",
  "description": "Switch built on Base UI; the thumb springs between states.",
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://seamui.dev/r/utils.json",
    "https://seamui.dev/r/motion.json",
    "https://seamui.dev/r/haptics.json"
  ],
  "files": [
    {
      "path": "registry/seam/ui/switch.tsx",
      "content": "\"use client\"\n\nimport type * as React from \"react\"\nimport { Switch as BaseSwitch } from \"@base-ui/react/switch\"\nimport { motion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  springs,\n  fades,\n  reduced,\n  useMounted,\n  useReducedMotion,\n} from \"@/lib/motion\"\nimport { useHaptics } from \"@/lib/haptics\"\n\nconst MotionThumb = motion.create(BaseSwitch.Thumb)\n\ntype SwitchSize = \"default\" | \"sm\"\n\n/**\n * Track/thumb geometry per size. The thumb's rest and pressed widths live here\n * too, because the press-stretch is an inline motion value (px) — a CSS class\n * alone can't scale it, and mismatched pairs distort the thumb.\n */\nconst SIZES: Record<\n  SwitchSize,\n  { track: string; thumb: string; rest: number; pressed: number }\n> = {\n  default: { track: \"h-5 w-9\", thumb: \"size-4\", rest: 16, pressed: 20 },\n  sm: { track: \"h-4 w-7\", thumb: \"size-3\", rest: 12, pressed: 15 },\n}\n\nfunction Switch({\n  className,\n  disabled,\n  onCheckedChange,\n  size = \"default\",\n  ...props\n}: React.ComponentProps<typeof BaseSwitch.Root> & { size?: SwitchSize }) {\n  const geometry = SIZES[size]\n  const reduceMotion = useReducedMotion()\n  const { trigger } = useHaptics()\n  // Defer motion's animated inline styles until after hydration: the width\n  // variant below serializes `width:16px` on the server that the client's\n  // first paint doesn't emit (the `size-4` class already sets it), so SSR and\n  // client disagree. Gating on mount lets the rest state fall back to the CSS\n  // width during hydration; the press-stretch still springs post-mount.\n  const mounted = useMounted()\n\n  return (\n    <BaseSwitch.Root\n      data-slot=\"switch\"\n      disabled={disabled}\n      // the render element below IS a native <button> — tell Base UI, since a\n      // custom render flips its default to \"assume non-button\".\n      nativeButton\n      // tactile feedback: a tick as the state commits (no-op sans provider).\n      onCheckedChange={(\n        ...args: Parameters<NonNullable<typeof onCheckedChange>>\n      ) => {\n        trigger(\"tick\")\n        onCheckedChange?.(...args)\n      }}\n      // seam touch feedback: while pressed the thumb *stretches* toward the\n      // far side (the \"pressed\" variant propagates down to the thumb), then\n      // snaps across on release — the iOS switch feel. Under reduced motion\n      // the press dims instead; the thumb still jumps states via layout.\n      render={\n        <motion.button\n          initial={false}\n          animate=\"rest\"\n          whileTap={\n            disabled ? undefined : reduceMotion ? reduced.pressed : \"pressed\"\n          }\n          transition={reduceMotion ? fades.fast : springs.press}\n        />\n      }\n      className={cn(\n        // track — the thumb rides from left to right via the flex justify swap.\n        \"inline-flex shrink-0 items-center rounded-full p-0.5 outline-none\",\n        geometry.track,\n        \"bg-input data-[checked]:bg-primary\",\n        \"justify-start data-[checked]:justify-end\",\n        \"focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      <MotionThumb\n        data-slot=\"switch-thumb\"\n        // seam motion: `layout` springs the thumb when justify flips on toggle.\n        layout\n        // width geometry for the press stretch (16 = size-4 at rest); the\n        // justify pin means growth always heads toward the far side. Applied\n        // only after mount so the resting width comes from CSS during SSR/\n        // hydration (avoids the serialized-inline-style mismatch).\n        variants={\n          mounted && !reduceMotion\n            ? {\n                rest: { width: geometry.rest },\n                pressed: { width: geometry.pressed },\n              }\n            : undefined\n        }\n        transition={reduceMotion ? reduced.instant : springs.snappy}\n        className={cn(\"bg-card rounded-full shadow-resting\", geometry.thumb)}\n      />\n    </BaseSwitch.Root>\n  )\n}\n\nexport { Switch }\n",
      "type": "registry:ui",
      "target": "components/ui/switch.tsx"
    }
  ]
}