{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "apple-switch",
  "title": "Apple Switch",
  "description": "Animated toggle switch with separate track/thumb sizing, spring feedback, optional label text, and tone variants.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/components/unlumen/apple-switch/index.tsx",
      "content": "\"use client\";\n\nimport { motion, useMotionValue, useSpring, useTransform } from \"motion/react\";\nimport { forwardRef, useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst switchSizes = {\n  sm: {\n    trackX: 46,\n    trackY: 24,\n    thumbX: 22,\n    thumbY: 18,\n    padding: 3,\n  },\n  md: {\n    trackX: 62,\n    trackY: 30,\n    thumbX: 32,\n    thumbY: 24,\n    padding: 4,\n  },\n  lg: {\n    trackX: 74,\n    trackY: 36,\n    thumbX: 34,\n    thumbY: 28,\n    padding: 5,\n  },\n} as const;\n\nconst switchTones = {\n  neutral: {\n    off: \"color-mix(in srgb, var(--muted) 82%, transparent)\",\n    on: \"#34c759\",\n    thumb: \"#ffffff\",\n    glow: \"color-mix(in srgb, #34c759 32%, transparent)\",\n  },\n  accent: {\n    off: \"color-mix(in srgb, var(--muted) 82%, transparent)\",\n    on: \"var(--foreground)\",\n    thumb: \"#ffffff\",\n    glow: \"color-mix(in srgb, var(--accent) 42%, transparent)\",\n  },\n} as const;\n\nconst thumbSpring = {\n  stiffness: 700,\n  damping: 48,\n  mass: 0.55,\n};\n\nconst grabSpring = {\n  stiffness: 500,\n  damping: 25,\n};\n\ninterface AppleSwitchProps\n  extends Omit<\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    \"onChange\" | \"role\"\n  > {\n  checked?: boolean;\n  defaultChecked?: boolean;\n  onCheckedChange?: (checked: boolean) => void;\n  label?: React.ReactNode;\n  description?: React.ReactNode;\n  /** @default \"md\" */\n  size?: keyof typeof switchSizes;\n  /** @default \"neutral\" */\n  tone?: keyof typeof switchTones;\n  /** @default \"right\" */\n  labelSide?: \"left\" | \"right\";\n}\n\nconst clamp = (value: number, min: number, max: number) => {\n  return Math.min(Math.max(value, min), max);\n};\n\nconst AppleSwitch = forwardRef<HTMLButtonElement, AppleSwitchProps>(\n  (\n    {\n      checked,\n      onCheckedChange,\n      label,\n      description,\n      size = \"md\",\n      tone = \"neutral\",\n      labelSide = \"right\",\n      className,\n      style,\n      disabled,\n      defaultChecked,\n      id,\n      type = \"button\",\n      onClick,\n      onPointerCancel,\n      onPointerDown,\n      onPointerLeave,\n      onPointerMove,\n      onPointerUp,\n      ...props\n    },\n    ref,\n  ) => {\n    const generatedId = useId();\n    const switchId = id ?? generatedId;\n    const [uncontrolledChecked, setUncontrolledChecked] = useState(\n      Boolean(defaultChecked),\n    );\n    const currentChecked = checked ?? uncontrolledChecked;\n    const metrics = switchSizes[size];\n    const colors = switchTones[tone];\n    const thumbTravel = metrics.trackX - metrics.thumbX - metrics.padding * 2;\n    const targetX = useMotionValue(currentChecked ? thumbTravel : 0);\n    const thumbX = useSpring(targetX, thumbSpring);\n    const grabTarget = useMotionValue(0);\n    const grabProgress = useSpring(grabTarget, grabSpring);\n    const thumbWidth = useTransform(\n      grabProgress,\n      [0, 1],\n      [metrics.thumbX, metrics.thumbX + metrics.padding * 4.5],\n    );\n    const thumbHeight = useTransform(\n      grabProgress,\n      [0, 1],\n      [metrics.thumbY, metrics.thumbY + metrics.padding * 2.3],\n    );\n    const thumbOffsetX = useTransform(\n      () => thumbX.get() - (thumbWidth.get() - metrics.thumbX) / 2,\n    );\n    const liquidOpacity = useTransform(grabProgress, [0, 1], [0, 0.76]);\n    const liquidScale = useTransform(grabProgress, [0, 1], [0.82, 1.08]);\n    const thumbOpacity = useTransform(grabProgress, [0, 1], [1, 0.2]);\n    const dragStartX = useRef(0);\n    const dragStartThumbX = useRef(0);\n    const isDragging = useRef(false);\n    const activePointerId = useRef<number | null>(null);\n    const suppressNextClick = useRef(false);\n    const activeProgress = useTransform(thumbX, [0, thumbTravel], [0, 1]);\n    const fillOpacity = useTransform(activeProgress, [0, 1], [0, 1]);\n    const glowOpacity = useTransform(\n      activeProgress,\n      [0, 0.7, 1],\n      [0, 0.18, 0.2],\n    );\n    const glowScale = useTransform(activeProgress, [0, 1], [0.82, 1]);\n\n    useEffect(() => {\n      if (activePointerId.current !== null) return;\n      targetX.set(currentChecked ? thumbTravel : 0);\n    }, [currentChecked, thumbTravel, targetX]);\n\n    const setChecked = (next: boolean) => {\n      if (next === currentChecked) {\n        targetX.set(next ? thumbTravel : 0);\n        return;\n      }\n\n      if (checked === undefined) {\n        setUncontrolledChecked(next);\n      }\n\n      targetX.set(next ? thumbTravel : 0);\n      onCheckedChange?.(next);\n    };\n\n    const handlePointerDown = (\n      event: React.PointerEvent<HTMLButtonElement>,\n    ) => {\n      onPointerDown?.(event);\n      if (event.defaultPrevented || disabled) return;\n      if (event.pointerType === \"mouse\" && event.button !== 0) return;\n      event.currentTarget.setPointerCapture(event.pointerId);\n      activePointerId.current = event.pointerId;\n      grabTarget.set(1);\n      dragStartX.current = event.clientX;\n      dragStartThumbX.current = thumbX.get();\n      targetX.set(dragStartThumbX.current);\n      isDragging.current = false;\n    };\n\n    const handlePointerMove = (\n      event: React.PointerEvent<HTMLButtonElement>,\n    ) => {\n      onPointerMove?.(event);\n      if (event.defaultPrevented || disabled) return;\n      if (\n        activePointerId.current !== null &&\n        event.pointerId !== activePointerId.current\n      ) {\n        return;\n      }\n      if (!event.currentTarget.hasPointerCapture(event.pointerId)) return;\n\n      const deltaX = event.clientX - dragStartX.current;\n\n      if (Math.abs(deltaX) > 3) {\n        isDragging.current = true;\n      }\n\n      if (!isDragging.current) return;\n      event.preventDefault();\n\n      const nextX = dragStartThumbX.current + deltaX;\n      targetX.set(clamp(nextX, 0, thumbTravel));\n    };\n\n    const handlePointerUp = (event: React.PointerEvent<HTMLButtonElement>) => {\n      onPointerUp?.(event);\n      if (\n        activePointerId.current !== null &&\n        event.pointerId !== activePointerId.current\n      ) {\n        return;\n      }\n      if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n        event.currentTarget.releasePointerCapture(event.pointerId);\n      }\n\n      activePointerId.current = null;\n      grabTarget.set(0);\n\n      if (!isDragging.current) return;\n\n      isDragging.current = false;\n      suppressNextClick.current = true;\n      setChecked(targetX.get() >= thumbTravel / 2);\n    };\n\n    const handlePointerCancel = (\n      event: React.PointerEvent<HTMLButtonElement>,\n    ) => {\n      onPointerCancel?.(event);\n      activePointerId.current = null;\n      isDragging.current = false;\n      grabTarget.set(0);\n      targetX.set(currentChecked ? thumbTravel : 0);\n    };\n\n    const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n      onClick?.(event);\n      if (event.defaultPrevented || disabled) return;\n\n      if (suppressNextClick.current) {\n        suppressNextClick.current = false;\n        event.preventDefault();\n        return;\n      }\n\n      setChecked(!currentChecked);\n    };\n\n    useEffect(() => {\n      const stopFromWindow = () => {\n        if (!isDragging.current && activePointerId.current === null) return;\n        const wasDragging = isDragging.current;\n        isDragging.current = false;\n        activePointerId.current = null;\n        grabTarget.set(0);\n        if (!wasDragging) return;\n        suppressNextClick.current = true;\n        setChecked(targetX.get() >= thumbTravel / 2);\n      };\n\n      window.addEventListener(\"pointerup\", stopFromWindow);\n      window.addEventListener(\"pointercancel\", stopFromWindow);\n      window.addEventListener(\"blur\", stopFromWindow);\n\n      return () => {\n        window.removeEventListener(\"pointerup\", stopFromWindow);\n        window.removeEventListener(\"pointercancel\", stopFromWindow);\n        window.removeEventListener(\"blur\", stopFromWindow);\n      };\n    });\n\n    const switchEl = (\n      <button\n        id={switchId}\n        ref={ref}\n        type={type}\n        role=\"switch\"\n        aria-checked={currentChecked}\n        disabled={disabled}\n        onClick={handleClick}\n        onPointerCancel={handlePointerCancel}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onPointerLeave={(event) => {\n          onPointerLeave?.(event);\n        }}\n        aria-label={typeof label === \"string\" ? label : props[\"aria-label\"]}\n        className={cn(\n          \"relative inline-flex shrink-0 cursor-pointer items-center rounded-full active:cursor-grabbing\",\n          \"border border-white/35 bg-white/10 shadow-inner backdrop-blur-md\",\n          \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          \"disabled:cursor-not-allowed disabled:opacity-45\",\n          className,\n        )}\n        style={{\n          width: metrics.trackX,\n          height: metrics.trackY,\n          touchAction: \"pan-y\",\n          ...style,\n        }}\n        {...props}\n      >\n        <motion.span\n          className=\"pointer-events-none absolute -inset-1 rounded-full blur-md\"\n          style={{\n            backgroundColor: colors.glow,\n            opacity: glowOpacity,\n            scale: glowScale,\n          }}\n        />\n\n        <span className=\"absolute inset-0 overflow-hidden rounded-full\">\n          <span\n            className=\"absolute inset-0 rounded-full\"\n            style={{\n              backgroundColor: colors.off,\n              boxShadow:\n                \"inset 0 1px 1px rgba(255,255,255,0.34), inset 0 -1px 2px rgba(0,0,0,0.08)\",\n            }}\n          />\n\n          <motion.span\n            className=\"absolute inset-0 rounded-full\"\n            style={{\n              backgroundColor: colors.on,\n              opacity: fillOpacity,\n            }}\n          />\n        </span>\n\n        <motion.span\n          className=\"pointer-events-none absolute left-0 z-[9] block rounded-full\"\n          style={{\n            width: thumbWidth,\n            height: thumbHeight,\n            x: thumbOffsetX,\n            top: \"50%\",\n            y: \"-50%\",\n            marginLeft: metrics.padding,\n            background:\n              \"color-mix(in srgb, var(--background) 82%, transparent)\",\n            opacity: liquidOpacity,\n            scale: liquidScale,\n            filter: \"blur(9px)\",\n            backdropFilter: \"blur(10px)\",\n            WebkitBackdropFilter: \"blur(10px)\",\n          }}\n        />\n\n        <motion.span\n          className=\"pointer-events-none z-10 block rounded-full\"\n          style={{\n            width: thumbWidth,\n            height: thumbHeight,\n            x: thumbOffsetX,\n            marginLeft: metrics.padding,\n            backgroundColor: colors.thumb,\n            opacity: thumbOpacity,\n            backdropFilter: \"blur(12px)\",\n            WebkitBackdropFilter: \"blur(12px)\",\n            boxShadow:\n              \"0 3px 11px rgba(0,0,0,0.24), 0 1px 1px rgba(0,0,0,0.12), inset 0 1px 0 rgba(255,255,255,0.78), inset 0 -1px 1px rgba(0,0,0,0.05)\",\n          }}\n        />\n      </button>\n    );\n\n    if (!label) return switchEl;\n\n    return (\n      <label\n        htmlFor={switchId}\n        className={cn(\n          \"inline-flex cursor-pointer select-none items-center gap-3\",\n          disabled && \"cursor-not-allowed opacity-50\",\n        )}\n      >\n        {labelSide === \"left\" && (\n          <span className=\"flex flex-col gap-0.5 text-right\">\n            <span className=\"text-sm font-medium text-foreground\">{label}</span>\n            {description && (\n              <span className=\"text-xs text-muted-foreground\">\n                {description}\n              </span>\n            )}\n          </span>\n        )}\n        {switchEl}\n        {labelSide === \"right\" && (\n          <span className=\"flex flex-col gap-0.5\">\n            <span className=\"text-sm font-medium text-foreground\">{label}</span>\n            {description && (\n              <span className=\"text-xs text-muted-foreground\">\n                {description}\n              </span>\n            )}\n          </span>\n        )}\n      </label>\n    );\n  },\n);\n\nAppleSwitch.displayName = \"AppleSwitch\";\n\nexport { AppleSwitch, type AppleSwitchProps };\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/apple-switch.tsx"
    }
  ],
  "type": "registry:ui"
}