{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "switch",
  "type": "registry:ui",
  "title": "Switch",
  "description": "Switch on Base UI whose thumb stretches on press then springs across, with sm and default sizes, a loading spinner state and an optional clickable label row.",
  "dependencies": [
    "@base-ui/react",
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/switch.tsx",
      "type": "registry:ui",
      "target": "components/ui/switch.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { Switch as SwitchPrimitive } from \"@base-ui/react/switch\"\nimport { LoaderCircleIcon } from \"lucide-react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Switch - hairline track with a raised thumb that leans on press, then springs across.\n * While pressed the thumb stretches toward where it is about to go. The track only changes colour.\n * 44px hit area via ::after. Pass `label` (+ `description`) for a settings row\n * where the whole row is clickable.\n */\n\ntype Size = \"sm\" | \"default\"\n\nconst SIZE: Record<Size, { track: string; thumb: string; rest: number; stretch: number; spinner: string; label: string }> = {\n  sm: { track: \"h-4 w-7\", thumb: \"h-3\", rest: 12, stretch: 4, spinner: \"size-2\", label: \"mt-0.5\" },\n  default: { track: \"h-5 w-9\", thumb: \"h-4\", rest: 16, stretch: 4, spinner: \"size-2.5\", label: \"\" },\n}\n\ntype SwitchProps = Omit<SwitchPrimitive.Root.Props, \"children\"> & {\n  size?: Size\n  /** Shows a spinner in the thumb. Stays focusable but can't be toggled. */\n  loading?: boolean\n  /** Renders a settings row: label on the left, switch on the right, whole row clickable. */\n  label?: React.ReactNode\n  description?: React.ReactNode\n}\n\nfunction chain<E>(a: ((e: E) => void) | undefined, b: (e: E) => void) {\n  return (e: E) => {\n    a?.(e)\n    b(e)\n  }\n}\n\nfunction Switch({\n  className,\n  size = \"default\",\n  loading = false,\n  label,\n  description,\n  checked: checkedProp,\n  defaultChecked = false,\n  onCheckedChange,\n  disabled,\n  readOnly,\n  onPointerDown,\n  onPointerUp,\n  onPointerLeave,\n  onPointerCancel,\n  onKeyDown,\n  onKeyUp,\n  onBlur,\n  ...props\n}: SwitchProps) {\n  const [inner, setInner] = React.useState(defaultChecked)\n  const checked = checkedProp ?? inner\n  const [pressed, setPressed] = React.useState(false)\n  const reduce = useReducedMotion()\n  const id = React.useId()\n  const s = SIZE[size]\n  const interactive = !disabled && !readOnly && !loading\n  const invalid = props[\"aria-invalid\"] === true || props[\"aria-invalid\"] === \"true\"\n  const row = label != null || description != null\n  const release = () => requestAnimationFrame(() => setPressed(false))\n\n  const press = {\n    onPointerDown: (e: React.PointerEvent<HTMLElement>) => {\n      if (interactive && e.button === 0) setPressed(true)\n    },\n    onPointerUp: release,\n    onPointerLeave: () => setPressed(false),\n    onPointerCancel: () => setPressed(false),\n  }\n\n  const control = (\n    <SwitchPrimitive.Root\n      data-slot=\"switch\"\n      data-size={size}\n      data-loading={loading || undefined}\n      checked={checked}\n      disabled={disabled}\n      readOnly={readOnly || loading}\n      aria-busy={loading || undefined}\n      aria-labelledby={row && label != null && !props[\"aria-label\"] ? `${id}-label` : undefined}\n      aria-describedby={row && description != null ? `${id}-desc` : undefined}\n      onCheckedChange={(next, details) => {\n        setPressed(false)\n        if (checkedProp === undefined) setInner(next)\n        onCheckedChange?.(next, details)\n      }}\n      onKeyDown={chain(onKeyDown, (e) => {\n        if (interactive && e.key === \" \") setPressed(true)\n      })}\n      onKeyUp={chain(onKeyUp, release)}\n      onBlur={chain(onBlur, () => setPressed(false))}\n      onPointerDown={row ? onPointerDown : chain(onPointerDown, press.onPointerDown)}\n      onPointerUp={row ? onPointerUp : chain(onPointerUp, press.onPointerUp)}\n      onPointerLeave={row ? onPointerLeave : chain(onPointerLeave, press.onPointerLeave)}\n      onPointerCancel={row ? onPointerCancel : chain(onPointerCancel, press.onPointerCancel)}\n      className={cn(\n        \"peer group/switch relative inline-flex shrink-0 cursor-pointer items-center rounded-full p-0.5 outline-none\",\n        \"bg-sunken transition-[background-color,box-shadow] duration-[140ms] ease-hairline\",\n        invalid\n          ? \"shadow-[inset_0_0_0_1px_var(--danger)]\"\n          : \"shadow-[inset_0_0_0_1px_var(--border-strong)] data-checked:shadow-[inset_0_0_0_1px_var(--accent)]\",\n        \"after:absolute after:-inset-x-2 after:-inset-y-3\",\n        \"focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg\",\n        \"data-checked:justify-end data-checked:bg-accent\",\n        \"hover:data-unchecked:bg-[color-mix(in_oklch,var(--sunken),var(--fg)_6%)] hover:data-checked:bg-accent-hover\",\n        row && \"group-hover/switch-row:data-unchecked:bg-[color-mix(in_oklch,var(--sunken),var(--fg)_6%)] group-hover/switch-row:data-checked:bg-accent-hover\",\n        readOnly && !loading && \"cursor-default\",\n        loading && \"cursor-progress\",\n        \"data-disabled:cursor-not-allowed data-disabled:opacity-45\",\n        s.track,\n        row && s.label,\n        !row && className\n      )}\n      {...props}\n    >\n      <motion.span\n        data-slot=\"switch-thumb\"\n        data-checked={checked || undefined}\n        data-pressed={pressed || undefined}\n        layout\n        layoutDependency={`${checked}-${pressed}`}\n        transition={reduce ? { duration: 0 } : { type: \"spring\", bounce: 0.18, duration: 0.42 }}\n        style={{ width: pressed ? s.rest + s.stretch : s.rest, borderRadius: 999 }}\n        className={cn(\"pointer-events-none grid shrink-0 place-items-center bg-raised shadow-key\", s.thumb)}\n      >\n        {loading ? (\n          <LoaderCircleIcon aria-hidden className={cn(\"animate-spin text-accent\", s.spinner)} strokeWidth={3} />\n        ) : null}\n      </motion.span>\n    </SwitchPrimitive.Root>\n  )\n\n  if (!row) return control\n\n  return (\n    <label\n      data-slot=\"switch-row\"\n      data-disabled={disabled || undefined}\n      {...press}\n      className={cn(\n        \"group/switch-row flex w-full cursor-pointer items-start justify-between gap-4 select-none\",\n        (disabled || readOnly) && \"cursor-default\",\n        loading && \"cursor-progress\",\n        disabled && \"cursor-not-allowed\",\n        className\n      )}\n    >\n      <span className={cn(\"grid min-w-0 gap-0.5\", disabled && \"opacity-60\")}>\n        {label != null ? (\n          <span id={`${id}-label`} className=\"text-[0.8125rem] leading-5 font-medium text-fg\">\n            {label}\n          </span>\n        ) : null}\n        {description != null ? (\n          <span id={`${id}-desc`} className=\"text-xs text-pretty text-fg-muted\">\n            {description}\n          </span>\n        ) : null}\n      </span>\n      {control}\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {loading ? \"Saving\" : \"\"}\n      </span>\n    </label>\n  )\n}\n\nexport { Switch }\nexport type { SwitchProps }\n"
    }
  ]
}
