{
  "name": "switch",
  "type": "registry:ui",
  "title": "Switch",
  "description": "Direct on-or-off preferences with native form state, labels, sizes and reduced-motion support.",
  "registryDependencies": [
    "@siteplane/base"
  ],
  "dependencies": [
    "@base-ui/react@1.5.0",
    "class-variance-authority@0.7.1"
  ],
  "files": [
    {
      "path": "src/components/ui/switch.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Switch as SwitchPrimitive } from \"@base-ui/react/switch\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  useSiteplaneUI,\n  useResolvedControlSize,\n} from \"@/components/ui/siteplane-provider\";\n\nexport const switchVariants = cva(\n  \"siteplane-switch-motion inline-flex h-[calc(var(--thumb-size)+2px)] w-[calc(var(--thumb-size)*2-2px)] shrink-0 cursor-pointer items-center p-0.5 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-disabled:cursor-not-allowed data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-64 not-data-disabled:data-unchecked:hover:bg-input/80 not-data-disabled:data-unchecked:hover:shadow-sm/10 dark:not-data-disabled:data-unchecked:hover:bg-white/12\",\n  {\n    defaultVariants: {\n      size: \"default\",\n    },\n    variants: {\n      size: {\n        default:\n          \"[--thumb-size:--spacing(5)] rounded-lg sm:[--thumb-size:--spacing(4)] sm:rounded-md\",\n        lg: \"[--thumb-size:--spacing(6)] rounded-xl sm:[--thumb-size:--spacing(5)] sm:rounded-lg\",\n        sm: \"[--thumb-size:--spacing(4)] rounded-md sm:[--thumb-size:--spacing(3.5)]\",\n        xl: \"[--thumb-size:--spacing(7)] rounded-2xl sm:[--thumb-size:--spacing(6)] sm:rounded-xl\",\n      },\n    },\n  },\n);\n\nexport interface SwitchProps\n  extends Omit<SwitchPrimitive.Root.Props, \"size\">,\n    VariantProps<typeof switchVariants> {}\n\ntype SwitchDebugInteraction = {\n  id: number;\n  source: string;\n  startedAt: number;\n};\n\nlet switchDebugInteractionId = 0;\n\nfunction getSwitchDebugNow() {\n  return typeof performance !== \"undefined\" ? performance.now() : Date.now();\n}\n\nfunction formatSwitchDebugMs(value: number) {\n  return Number(value.toFixed(1));\n}\n\nfunction getSwitchDebugStorageValue(key: string) {\n  if (typeof window === \"undefined\") {\n    return null;\n  }\n\n  try {\n    return window.localStorage.getItem(key);\n  } catch {\n    return null;\n  }\n}\n\nfunction isSwitchDebugEnabled() {\n  return getSwitchDebugStorageValue(\"siteplane:switch-debug\") !== \"off\";\n}\n\nfunction shouldLogSwitchDebugSnapshot() {\n  return getSwitchDebugStorageValue(\"siteplane:switch-debug-snapshot\") === \"on\";\n}\n\nfunction getSwitchDebugSnapshot(root: HTMLElement | null, includeComputed: boolean) {\n  const thumb = root?.querySelector<HTMLElement>('[data-slot=\"switch-thumb\"]') ?? null;\n  const label = root?.closest(\"label\") ?? null;\n  const base = {\n    ariaLabel: root?.getAttribute(\"aria-label\") ?? null,\n    labelText: label?.textContent?.replace(/\\s+/g, \" \").trim() ?? null,\n    dataChecked: root?.hasAttribute(\"data-checked\") ?? null,\n    dataUnchecked: root?.hasAttribute(\"data-unchecked\") ?? null,\n    ariaChecked: root?.getAttribute(\"aria-checked\") ?? null,\n  };\n\n  if (!includeComputed) {\n    return base;\n  }\n\n  const rootStyle = root ? getComputedStyle(root) : null;\n  const thumbStyle = thumb ? getComputedStyle(thumb) : null;\n\n  return {\n    ...base,\n    rootRadius: rootStyle?.borderRadius ?? null,\n    thumbRadius: thumbStyle?.borderRadius ?? null,\n    thumbTranslate: thumbStyle?.translate ?? null,\n    thumbTransform: thumbStyle?.transform ?? null,\n    thumbTransition: thumbStyle?.transition ?? null,\n    radiusVar: rootStyle?.getPropertyValue(\"--radius\").trim() ?? null,\n    radiusLg: rootStyle?.getPropertyValue(\"--radius-lg\").trim() ?? null,\n    radiusMd: rootStyle?.getPropertyValue(\"--radius-md\").trim() ?? null,\n    thumbSize: rootStyle?.getPropertyValue(\"--thumb-size\").trim() ?? null,\n  };\n}\n\nfunction logSwitchDebug(\n  phase: string,\n  interaction: SwitchDebugInteraction,\n  root: HTMLElement | null,\n  extra?: Record<string, unknown>,\n) {\n  const now = getSwitchDebugNow();\n\n  console.log(\"[siteplane:switch-debug]\", {\n    id: interaction.id,\n    phase,\n    source: interaction.source,\n    dtMs: formatSwitchDebugMs(now - interaction.startedAt),\n    nowMs: formatSwitchDebugMs(now),\n    ...extra,\n    ...getSwitchDebugSnapshot(root, shouldLogSwitchDebugSnapshot()),\n  });\n}\n\nfunction observeSwitchDebugLongTasks(\n  interaction: SwitchDebugInteraction,\n  rootRef: React.RefObject<HTMLElement | null>,\n) {\n  if (\n    typeof window === \"undefined\" ||\n    typeof PerformanceObserver === \"undefined\" ||\n    !PerformanceObserver.supportedEntryTypes.includes(\"longtask\")\n  ) {\n    return;\n  }\n\n  const observer = new PerformanceObserver((list) => {\n    for (const entry of list.getEntries()) {\n      if (entry.startTime < interaction.startedAt) {\n        continue;\n      }\n\n      logSwitchDebug(\"longtask\", interaction, rootRef.current, {\n        longTaskStartDtMs: formatSwitchDebugMs(entry.startTime - interaction.startedAt),\n        longTaskDurationMs: formatSwitchDebugMs(entry.duration),\n      });\n    }\n  });\n\n  observer.observe({ entryTypes: [\"longtask\"] });\n\n  window.setTimeout(() => {\n    observer.disconnect();\n  }, 1300);\n}\n\nfunction scheduleSwitchDebugFrames(\n  interaction: SwitchDebugInteraction,\n  rootRef: React.RefObject<HTMLElement | null>,\n  extra?: Record<string, unknown>,\n) {\n  if (typeof window === \"undefined\") {\n    return;\n  }\n\n  observeSwitchDebugLongTasks(interaction, rootRef);\n\n  requestAnimationFrame(() => {\n    logSwitchDebug(\"frame-1\", interaction, rootRef.current, extra);\n\n    requestAnimationFrame(() => {\n      logSwitchDebug(\"frame-2\", interaction, rootRef.current, extra);\n    });\n  });\n\n  for (const delay of [150, 500, 1000]) {\n    window.setTimeout(() => {\n      logSwitchDebug(`timeout-${delay}`, interaction, rootRef.current, extra);\n    }, delay);\n  }\n}\n\nexport function Switch({\n  className,\n  onCheckedChange,\n  onClick,\n  onKeyDown,\n  onPointerDown,\n  size,\n  ...props\n}: SwitchProps): React.ReactElement {\n  const resolvedSize = useResolvedControlSize(size);\n  const { radius } = useSiteplaneUI();\n  const rootRef = React.useRef<HTMLElement | null>(null);\n  const debugInteractionRef = React.useRef<SwitchDebugInteraction | null>(null);\n  const shouldDebugSwitch =\n    process.env.NODE_ENV !== \"production\" && radius === \"none\" && isSwitchDebugEnabled();\n\n  function beginDebugInteraction(source: string) {\n    if (!shouldDebugSwitch) {\n      return null;\n    }\n\n    const interaction = {\n      id: ++switchDebugInteractionId,\n      source,\n      startedAt: getSwitchDebugNow(),\n    };\n\n    debugInteractionRef.current = interaction;\n\n    return interaction;\n  }\n\n  function getDebugInteraction(source: string) {\n    if (!shouldDebugSwitch) {\n      return null;\n    }\n\n    return debugInteractionRef.current ?? beginDebugInteraction(source);\n  }\n\n  return (\n    <SwitchPrimitive.Root\n      className={cn(switchVariants({ size: resolvedSize }), className)}\n      data-size={resolvedSize}\n      data-slot=\"switch\"\n      onCheckedChange={(checked, eventDetails) => {\n        const interaction = getDebugInteraction(\"checked-change\");\n\n        if (interaction) {\n          logSwitchDebug(\"checked-change\", interaction, rootRef.current, {\n            nextChecked: checked,\n            eventType: eventDetails.event?.type ?? null,\n          });\n          scheduleSwitchDebugFrames(interaction, rootRef, { nextChecked: checked });\n        }\n\n        onCheckedChange?.(checked, eventDetails);\n      }}\n      onClick={(event) => {\n        const interaction = getDebugInteraction(\"click\") ?? beginDebugInteraction(\"click\");\n\n        if (interaction) {\n          logSwitchDebug(\"click\", interaction, rootRef.current, {\n            pointerType: \"pointerType\" in event ? event.pointerType : null,\n          });\n        }\n\n        onClick?.(event);\n      }}\n      onKeyDown={(event) => {\n        if (event.key === \" \" || event.key === \"Enter\") {\n          const interaction = beginDebugInteraction(\n            `key-${event.key === \" \" ? \"space\" : \"enter\"}`,\n          );\n\n          if (interaction) {\n            logSwitchDebug(\"keydown\", interaction, rootRef.current, {\n              key: event.key,\n            });\n          }\n        }\n\n        onKeyDown?.(event);\n      }}\n      onPointerDown={(event) => {\n        const interaction = beginDebugInteraction(\"pointerdown\");\n\n        if (interaction) {\n          logSwitchDebug(\"pointerdown\", interaction, rootRef.current, {\n            pointerType: event.pointerType,\n            button: event.button,\n          });\n        }\n\n        onPointerDown?.(event);\n      }}\n      ref={rootRef}\n      {...props}\n    >\n      <SwitchPrimitive.Thumb\n        className={cn(\n          \"pointer-events-none block aspect-square h-full origin-left rounded-[inherit] bg-background shadow-sm/5 will-change-transform in-[[role=switch]:active,[data-slot=label]:active,[data-slot=field-label]:active]:not-data-disabled:scale-x-110 data-checked:origin-[var(--thumb-size)_50%] data-checked:translate-x-[calc(var(--thumb-size)-4px)]\",\n        )}\n        data-slot=\"switch-thumb\"\n      />\n    </SwitchPrimitive.Root>\n  );\n}\n\nexport { SwitchPrimitive };\n"
    }
  ],
  "categories": [
    "form-controls"
  ],
  "docs": "# Switch\n\n> Siteplane UI release `0.1.1`.\n\n## Public Purpose\n\nUse Switch for a direct on/off preference whose change is understood as enabling or disabling a setting.\n\n## Import\n\n`import { Switch } from \"@/components/ui/switch\";`\n\n`import { Field, FieldDescription, FieldError, FieldLabel } from \"@/components/ui/field\";`\n\n`import { Form } from \"@/components/ui/form\";`\n\n`import { Label } from \"@/components/ui/label\";`\n\n## Public API\n\n- `Switch` accepts controlled `checked`, uncontrolled `defaultChecked`, `onCheckedChange`, `disabled`, `readOnly`, `required`, `name`, `value` and Base UI switch props.\n- `size=\"sm\" | \"default\" | \"lg\" | \"xl\"` overrides the nearest `SiteplaneUIProvider` control size.\n- The native control mirrors checked, disabled and invalid state through Base UI attributes and form participation.\n- Every size keeps a consistent 2 px inset between track and thumb without changing the outer track dimensions.\n- Track radius scales with control size through the existing provider-derived radius tokens, and the thumb inherits the same radius, including `radius=\"none\"`.\n\n## Public Motion\n\n- The thumb translates and stretches through the native Siteplane Switch motion contract; hover and press feedback stay inside the primitive.\n- Reduced motion keeps the checked state immediate and removes non-essential transition.\n\n## Public Accessibility\n\n- Pair every Switch with a visible clickable label or provide a precise accessible name.\n- Use Switch for settings, Checkbox for form agreement or multi-select, and never communicate state by color alone.\n- Disabled, read-only and invalid state need supporting context when the reason is not obvious.\n\n## Public Agent Guidance\n\n- Use only the documented sizes or provider default.\n- Keep label and control as one clickable setting row without nesting unrelated actions.\n- Do not rebuild the switch track or thumb from raw elements.\n\n## Public Links\n\n- [Documentation and preview](https://ui.siteplane.io/docs/components/form-controls/switch)\n\n- [Registry source](https://ui.siteplane.io/r/switch.json)\n",
  "meta": {
    "siteplane:agentContractVersion": 1,
    "siteplane:docs": "https://ui.siteplane.io/docs/components/form-controls/switch",
    "siteplane:releaseVersion": "0.1.1",
    "siteplane:sourceOwned": true
  },
  "devDependencies": []
}
