{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "halo-switch",
  "type": "registry:ui",
  "description": "Toggle switch with spring thumb, frosted track, and extended touch hit area",
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "files": [
    {
      "path": "registry/default/ui/halo-switch.tsx",
      "content": "\"use client\"\n\n/**\n * Toggle switch aligned with {@link HaloSegmented}: same layout spring, frosted track,\n * delicate rim + soft shadows, spring thumb travel (`transform` only). Extended hit target\n * via `::after` (≥44px touch area, touch-first).\n */\nimport {\n  useCallback,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n  type CSSProperties,\n  type Ref,\n  type RefCallback,\n} from \"react\"\nimport { Switch as SwitchPrimitive } from \"@base-ui/react/switch\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/** Matches `layoutSpring` in `halo-button.tsx` / `halo-segmented.tsx`. */\nconst layoutSpring = {\n  type: \"spring\" as const,\n  bounce: 0.12,\n  damping: 34,\n  stiffness: 420,\n}\n\n/** Horizontal inset from track edge to thumb (must match `px-[…]` on the switch root). */\nconst TRACK_PAD_X_PX = 3\n\n/** Approximate thumb travel before layout measure (avoids one-frame jump). */\nfunction approximateTravelPx(size: \"sm\" | \"default\") {\n  const trackW = size === \"sm\" ? 34 : 40\n  const thumbW = size === \"sm\" ? 12 : 14\n  return Math.max(0, trackW - thumbW - TRACK_PAD_X_PX * 2)\n}\n\nfunction mergeRefs<T>(...refs: (Ref<T> | undefined)[]) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (ref == null) {\n        continue\n      }\n      if (typeof ref === \"function\") {\n        ;(ref as RefCallback<T>)(node)\n      } else {\n        ;(ref as { current: T | null }).current = node\n      }\n    }\n  }\n}\n\nexport interface HaloSwitchProps extends SwitchPrimitive.Root.Props {\n  /** Frosted shell + brand-tinted underglow (aligned with halo-card `primaryBg` family). */\n  translucent?: boolean\n  size?: \"sm\" | \"default\"\n}\n\nexport function HaloSwitch({\n  className,\n  translucent = true,\n  size = \"default\",\n  checked: checkedProp,\n  defaultChecked,\n  onCheckedChange,\n  disabled,\n  ...rootProps\n}: HaloSwitchProps) {\n  const reduceMotion = useReducedMotion() ?? false\n  const trackRef = useRef<HTMLSpanElement | null>(null)\n  const thumbRef = useRef<HTMLSpanElement | null>(null)\n  const [thumbTravel, setThumbTravel] = useState(() =>\n    approximateTravelPx(size)\n  )\n\n  const isControlled = checkedProp !== undefined\n  const [, setUncontrolled] = useState(Boolean(defaultChecked))\n\n  const measure = useCallback(() => {\n    const track = trackRef.current\n    const thumb = thumbRef.current\n    if (!(track && thumb)) {\n      return\n    }\n    const hw = thumb.getBoundingClientRect().width\n    const styles = getComputedStyle(track)\n    const padX =\n      Number.parseFloat(styles.paddingLeft) +\n      Number.parseFloat(styles.paddingRight)\n    /** `clientWidth` excludes border; subtract padding for the row the thumb slides in. */\n    const innerW = track.clientWidth - padX\n    const next = Math.max(0, innerW - hw)\n    setThumbTravel(next)\n  }, [])\n\n  useLayoutEffect(() => {\n    measure()\n  }, [measure])\n\n  useLayoutEffect(() => {\n    const track = trackRef.current\n    if (!track || typeof ResizeObserver === \"undefined\") {\n      return\n    }\n    const ro = new ResizeObserver(() => measure())\n    ro.observe(track)\n    return () => ro.disconnect()\n  }, [measure])\n\n  const handleCheckedChange = useCallback(\n    (\n      next: boolean,\n      details: Parameters<NonNullable<typeof onCheckedChange>>[1]\n    ) => {\n      if (!isControlled) {\n        setUncontrolled(next)\n      }\n      onCheckedChange?.(next, details)\n    },\n    [isControlled, onCheckedChange]\n  )\n\n  const thumbSize =\n    size === \"sm\" ? \"size-3 min-h-3 min-w-3\" : \"size-3.5 min-h-3.5 min-w-3.5\"\n  const trackSize =\n    size === \"sm\"\n      ? \"h-[18px] min-h-[18px] w-[34px] min-w-[34px]\"\n      : \"h-[22px] min-h-[22px] w-10 min-w-[40px]\"\n  /** `::after` extends hit area to ≥44px tall (Emil touch-first). */\n  const hitAfterY =\n    size === \"sm\" ? \"after:-inset-y-[13px]\" : \"after:-inset-y-[11px]\"\n\n  /** Shell fill: muted tints of the brand ramp (#ff0080 → #7928ca → #00d4ff), pairs with checked track. */\n  const shellBackgroundTranslucent =\n    \"linear-gradient(to bottom, color-mix(in oklch, var(--card) 82%, oklch(0.92 0.04 330)) 0%, color-mix(in oklch, var(--card) 78%, oklch(0.88 0.06 285)) 50%, color-mix(in oklch, var(--card) 76%, oklch(0.9 0.05 245)) 100%)\"\n  const shellBackgroundSolid =\n    \"linear-gradient(to bottom, color-mix(in oklch, var(--card) 95%, oklch(0.97 0.02 315)) 0%, color-mix(in oklch, var(--card) 93%, oklch(0.96 0.025 280)) 100%)\"\n\n  return (\n    <div\n      className={cn(\n        \"relative inline-flex max-w-full rounded-[9999px]\",\n        className\n      )}\n    >\n      {/* Underglow clipped to the same pill as the shell — avoids a second “rim” vs the track. */}\n      <div className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-[9999px]\">\n        {translucent ? (\n          <div\n            className={cn(\n              \"absolute inset-0\",\n              reduceMotion ? \"opacity-[0.38]\" : \"opacity-50\"\n            )}\n          >\n            <div\n              className=\"absolute inset-0 blur-2xl\"\n              style={{\n                background:\n                  \"radial-gradient(90% 70% at 50% 100%, rgba(255,0,128,0.28), rgba(121,40,202,0.12) 48%, transparent 62%)\",\n              }}\n            />\n            <div\n              className=\"absolute inset-0 opacity-70 blur-xl dark:opacity-50\"\n              style={{\n                background:\n                  \"radial-gradient(70% 55% at 30% 0%, rgba(0,212,255,0.22), rgba(0,112,243,0.08) 45%, transparent 58%)\",\n              }}\n            />\n          </div>\n        ) : null}\n      </div>\n\n      <div\n        className={cn(\n          \"relative rounded-[9999px] border border-[color-mix(in_oklch,var(--border)_72%,oklch(0.88_0.06_300))]\",\n          \"shadow-[0_1px_0_rgba(255,255,255,0.55)_inset,0_1px_2px_rgba(0,0,0,0.03),0_6px_18px_rgba(0,0,0,0.04)]\",\n          \"dark:border-border/80 dark:shadow-[0_1px_0_rgba(255,255,255,0.05)_inset,0_1px_2px_rgba(0,0,0,0.28),0_6px_22px_rgba(0,0,0,0.32)]\",\n          translucent && \"backdrop-blur-xl backdrop-saturate-150\"\n        )}\n        style={{\n          background: translucent\n            ? shellBackgroundTranslucent\n            : shellBackgroundSolid,\n        }}\n      >\n        <SwitchPrimitive.Root\n          {...rootProps}\n          checked={isControlled ? checkedProp : undefined}\n          className={cn(\n            /* Flex centers the thumb vertically; avoid absolute + nested flex (subpixel drift). */\n            \"group/switch relative isolate flex shrink-0 cursor-pointer flex-row items-center justify-start rounded-[9999px] border border-transparent px-[3px] outline-none\",\n            \"transition-[background,box-shadow] duration-200 ease-out\",\n            /* Hit slop only — must not paint (no blur/double rim). */\n            \"after:-inset-x-3 after:pointer-events-auto after:absolute after:z-0 after:rounded-[9999px] after:bg-transparent after:content-['']\",\n            hitAfterY,\n            \"focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30\",\n            \"data-unchecked:bg-[color-mix(in_oklch,var(--muted)_36%,oklch(0.945_0.032_295))]\",\n            /* Solid oklch ramp: same mix bases as unchecked/shell, stronger chroma (magenta → purple → cyan) — no alpha, no track blur */\n            \"data-checked:bg-[linear-gradient(180deg,color-mix(in_oklch,var(--muted)_32%,oklch(0.9_0.045_325))_0%,color-mix(in_oklch,var(--muted)_30%,oklch(0.87_0.068_290))_38%,color-mix(in_oklch,var(--muted)_30%,oklch(0.87_0.065_250))_72%,color-mix(in_oklch,var(--muted)_32%,oklch(0.85_0.072_268))_100%)]\",\n            \"dark:data-checked:bg-[linear-gradient(180deg,color-mix(in_oklch,var(--input)_80%,oklch(0.52_0.11_318))_0%,color-mix(in_oklch,var(--input)_78%,oklch(0.5_0.13_282))_36%,color-mix(in_oklch,var(--input)_78%,oklch(0.5_0.12_248))_70%,color-mix(in_oklch,var(--input)_80%,oklch(0.48_0.13_268))_100%)]\",\n            \"data-disabled:cursor-not-allowed data-disabled:opacity-50\",\n            \"dark:data-unchecked:bg-[color-mix(in_oklch,var(--input)_86%,oklch(0.38_0.035_280))]\",\n            trackSize\n          )}\n          data-size={size}\n          data-slot=\"halo-switch\"\n          defaultChecked={isControlled ? undefined : defaultChecked}\n          disabled={disabled}\n          onCheckedChange={handleCheckedChange}\n          ref={trackRef}\n        >\n          <SwitchPrimitive.Thumb\n            data-slot=\"halo-switch-thumb\"\n            render={(thumbProps, thumbState) => {\n              const {\n                className: thumbClassName,\n                ref: thumbRefProp,\n                style: thumbStyle,\n                // DOM handlers that share names with Motion APIs but have incompatible typings.\n                onDrag: _onDrag,\n                onDragStart: _onDragStart,\n                onDragEnd: _onDragEnd,\n                onAnimationStart: _onAnimationStart,\n                onAnimationEnd: _onAnimationEnd,\n                ...thumbRest\n              } = thumbProps\n              const { transform: _, ...thumbStyleSafe } = (thumbStyle ??\n                {}) as CSSProperties\n              return (\n                <motion.span\n                  {...thumbRest}\n                  animate={{\n                    x: thumbState.checked ? thumbTravel : 0,\n                  }}\n                  className={cn(\n                    thumbClassName,\n                    \"pointer-events-none relative z-1 box-border block shrink-0 rounded-full bg-background ring-0\",\n                    thumbSize,\n                    \"shadow-[0_1px_0_rgba(255,255,255,0.85)_inset,0_1px_2px_rgba(0,0,0,0.07),0_2px_10px_rgba(0,0,0,0.05)]\",\n                    \"ring-1 ring-black/8 dark:ring-white/12\",\n                    \"dark:shadow-[0_1px_0_rgba(255,255,255,0.12)_inset,0_1px_3px_rgba(0,0,0,0.45),0_2px_12px_rgba(0,0,0,0.35)]\",\n                    \"fine-hover:group-hover/switch:ring-2 fine-hover:group-hover/switch:ring-ring/20\",\n                    \"data-checked:bg-white dark:data-checked:bg-white dark:data-unchecked:bg-[oklch(0.97_0.01_280)]\"\n                  )}\n                  initial={false}\n                  ref={mergeRefs(thumbRefProp, thumbRef)}\n                  style={{\n                    ...thumbStyleSafe,\n                    willChange: reduceMotion ? undefined : \"transform\",\n                  }}\n                  transition={reduceMotion ? { duration: 0 } : layoutSpring}\n                  whileTap={reduceMotion ? undefined : { scale: 0.96 }}\n                />\n              )\n            }}\n          />\n        </SwitchPrimitive.Root>\n      </div>\n    </div>\n  )\n}\n\nexport type HaloSwitchThumbProps = ComponentProps<typeof SwitchPrimitive.Thumb>\n",
      "type": "registry:ui"
    }
  ]
}