{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slide-to-unlock",
  "title": "Slide to Unlock",
  "author": "jyotirmoydas <jyotirmoydas12345@gmail.com>",
  "description": "An iOS 26-inspired liquid glass slide-to-unlock switch with refraction effects.",
  "dependencies": ["cn", "motion", "sonner"],
  "registryDependencies": [
    "https://jyotimoydas.in/r/use-sound.json",
    "https://jyotimoydas.in/r/liquid-glass-lib.json"
  ],
  "files": [
    {
      "path": "src/registry/slide-to-unlock/slide-to-unlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport {\n  mix,\n  motion,\n  useMotionValue,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\nimport React, { useCallback, useEffect } from \"react\";\nimport { toast } from \"sonner\";\n\nimport { useSound } from \"@/hooks/use-sound\";\nimport { RefractionFilter } from \"@/registry/liquid-glass-lib/refraction-filter\";\n\n/**\n * Props for the SlideToUnlock component.\n */\nexport interface SlideToUnlockProps {\n  // ─────────────────────────────────────────────────────────────────────────\n  // Callbacks\n  // ─────────────────────────────────────────────────────────────────────────\n\n  /**\n   * Callback triggered when the slider reaches the ON position.\n   */\n  onUnlock?: () => void;\n\n  /**\n   * Callback triggered when the slider reaches the OFF position.\n   */\n  onLock?: () => void;\n\n  // ─────────────────────────────────────────────────────────────────────────\n  // Behavior\n  // ─────────────────────────────────────────────────────────────────────────\n\n  /**\n   * Initial state of the switch.\n   * @default false (OFF position)\n   */\n  defaultChecked?: boolean;\n\n  /**\n   * Whether to play the unlock sound effect.\n   * @default true\n   */\n  soundEnabled?: boolean;\n\n  /**\n   * Whether to show a toast notification on unlock.\n   * @default true\n   */\n  toastEnabled?: boolean;\n\n  /**\n   * Custom message to display in the unlock toast.\n   * @default \"Unlocked\"\n   */\n  toastMessage?: string;\n\n  /**\n   * Whether the switch is disabled.\n   * @default false\n   */\n  disabled?: boolean;\n\n  // ─────────────────────────────────────────────────────────────────────────\n  // Appearance\n  // ─────────────────────────────────────────────────────────────────────────\n\n  /**\n   * Additional CSS classes to apply to the switch.\n   */\n  className?: string;\n\n  /**\n   * Color of the thumb/handle when not pressed.\n   * @default \"rgba(255, 255, 255, 1)\"\n   */\n  thumbColor?: string;\n\n  /**\n   * Background color of the track when OFF.\n   * @default \"#94949F77\"\n   */\n  trackColorOff?: string;\n\n  /**\n   * Background color of the track when ON.\n   * @default \"#3BBF4EEE\"\n   */\n  trackColorOn?: string;\n\n  // ─────────────────────────────────────────────────────────────────────────\n  // Optical Parameters (Advanced)\n  // ─────────────────────────────────────────────────────────────────────────\n\n  /**\n   * Blur level for the glass refraction effect.\n   * @default 0.2\n   */\n  blur?: number;\n\n  /**\n   * Opacity of the specular highlight.\n   * @default 0.5\n   */\n  specularOpacity?: number;\n\n  /**\n   * Saturation multiplier for the specular highlight.\n   * @default 6\n   */\n  specularSaturation?: number;\n\n  /**\n   * Base refraction level (controls magnification intensity).\n   * @default 1\n   */\n  refractionLevel?: number;\n}\n\n/**\n * A sleek, iOS 26-inspired liquid glass slide-to-unlock switch with\n * real-time SVG refraction effects.\n *\n * @example\n * ```tsx\n * <SlideToUnlock\n *   onUnlock={() => console.log(\"Unlocked!\")}\n *   toastMessage=\"Access granted\"\n * />\n * ```\n *\n * @example Custom colors\n * ```tsx\n * <SlideToUnlock\n *   thumbColor=\"rgba(59, 130, 246, 0.9)\"\n *   trackColorOff=\"#1e293b\"\n *   trackColorOn=\"#3b82f6\"\n * />\n * ```\n *\n * @example Custom optical parameters\n * ```tsx\n * <SlideToUnlock\n *   blur={0.3}\n *   specularOpacity={0.7}\n *   specularSaturation={8}\n *   refractionLevel={1.2}\n * />\n * ```\n */\nexport const SlideToUnlock: React.FC<SlideToUnlockProps> = ({\n  // Callbacks\n  onUnlock,\n  onLock,\n  // Behavior\n  defaultChecked = false,\n  soundEnabled = true,\n  toastEnabled = true,\n  toastMessage = \"Unlocked\",\n  disabled = false,\n  // Appearance\n  className,\n  thumbColor = \"rgba(255, 255, 255, 1)\",\n  trackColorOff = \"#94949F77\",\n  trackColorOn = \"#3BBF4EEE\",\n  // Optical Parameters\n  blur = 0.2,\n  specularOpacity = 0.5,\n  specularSaturation = 6,\n  refractionLevel = 1,\n}) => {\n  // CONSTANTS (layout)\n  const sliderHeight = 67;\n  const sliderWidth = 160;\n  const thumbWidth = 146;\n  const thumbHeight = 92;\n  const thumbRadius = thumbHeight / 2;\n  const sliderRef = React.useRef<HTMLDivElement>(null);\n  const xDragRatio = useMotionValue(0);\n  const playUnlockSound = useSound(\"/audio/ui-sounds/unlock.wav\");\n\n  const THUMB_REST_SCALE = 0.65;\n  const THUMB_ACTIVE_SCALE = 0.9;\n\n  const THUMB_REST_OFFSET = ((1 - THUMB_REST_SCALE) * thumbWidth) / 2;\n\n  const TRAVEL =\n    sliderWidth - sliderHeight - (thumbWidth - thumbHeight) * THUMB_REST_SCALE;\n\n  // MOTION SOURCES\n  const checked = useMotionValue(defaultChecked ? 1 : 0);\n  const pointerDown = useMotionValue(0);\n  const initialPointerX = useMotionValue(0);\n  const active = useTransform(() => (pointerDown.get() > 0.5 ? 1 : 0));\n\n  // Helper to handle state change with callbacks\n  const handleStateChange = useCallback(\n    (newChecked: boolean, wasChecked: boolean) => {\n      if (newChecked && !wasChecked) {\n        // Transitioning to ON\n        if (soundEnabled) {\n          playUnlockSound();\n        }\n        if (toastEnabled) {\n          toast.success(toastMessage);\n        }\n        onUnlock?.();\n      } else if (!newChecked && wasChecked) {\n        // Transitioning to OFF\n        onLock?.();\n      }\n    },\n    [\n      soundEnabled,\n      toastEnabled,\n      toastMessage,\n      onUnlock,\n      onLock,\n      playUnlockSound,\n    ]\n  );\n\n  // GLOBAL POINTER-UP LISTENER\n  useEffect(() => {\n    const onPointerUp = (e: MouseEvent | TouchEvent) => {\n      if (disabled) return;\n      pointerDown.set(0);\n\n      const x =\n        e instanceof MouseEvent ? e.clientX : e.changedTouches[0].clientX;\n\n      const distance = x - initialPointerX.get();\n      if (Math.abs(distance) > 4) {\n        const dragX = xDragRatio.get();\n        const shouldBeChecked = dragX > 0.5;\n        const wasChecked = checked.get() > 0.5;\n        checked.set(shouldBeChecked ? 1 : 0);\n        handleStateChange(shouldBeChecked, wasChecked);\n      }\n    };\n\n    window.addEventListener(\"mouseup\", onPointerUp);\n    window.addEventListener(\"touchend\", onPointerUp);\n    return () => {\n      window.removeEventListener(\"mouseup\", onPointerUp);\n      window.removeEventListener(\"touchend\", onPointerUp);\n    };\n  }, [\n    disabled,\n    xDragRatio,\n    checked,\n    pointerDown,\n    initialPointerX,\n    handleStateChange,\n  ]);\n\n  // SPRINGS\n  const xRatio = useSpring(\n    useTransform(() => {\n      const c = checked.get();\n      const dragRatio = xDragRatio.get();\n\n      if (pointerDown.get() > 0.5) {\n        return dragRatio;\n      } else {\n        return c ? 1 : 0;\n      }\n    }),\n    { damping: 80, stiffness: 1000 }\n  );\n  const backgroundOpacity = useSpring(\n    useTransform(active, (v) => 1 - 0.9 * v),\n    { damping: 80, stiffness: 2000 }\n  );\n  const thumbScale = useSpring(\n    useTransform(\n      active,\n      (v) => THUMB_REST_SCALE + (THUMB_ACTIVE_SCALE - THUMB_REST_SCALE) * v\n    ),\n    { damping: 80, stiffness: 2000 }\n  );\n  const scaleRatio = useSpring(\n    useTransform(() => (0.4 + 0.5 * active.get()) * refractionLevel)\n  );\n  const considerChecked = useTransform(() => {\n    const x = xDragRatio.get();\n    const c = checked.get();\n    return pointerDown.get() ? (x > 0.5 ? 1 : 0) : c > 0.5 ? 1 : (0 as number);\n  });\n\n  const backgroundColor = useTransform(\n    useSpring(considerChecked, { damping: 80, stiffness: 1000 }),\n    mix(trackColorOff, trackColorOn)\n  );\n\n  // Parse thumb color to extract RGB for opacity animation\n  const thumbBaseColor = thumbColor;\n\n  const handleMouseMove = (e: React.MouseEvent) => {\n    if (disabled || !sliderRef.current) return;\n    e.stopPropagation();\n    const baseRatio = checked.get();\n    const clientX = e.clientX;\n    const displacementX = clientX - initialPointerX.get();\n    const ratio = baseRatio + displacementX / TRAVEL;\n    const overflow = ratio < 0 ? -ratio : ratio > 1 ? ratio - 1 : 0;\n    const overflowSign = ratio < 0 ? -1 : 1;\n    const dampedOverflow = (overflowSign * overflow) / 22;\n    xDragRatio.set(Math.min(1, Math.max(0, ratio)) + dampedOverflow);\n  };\n\n  const handleTouchMove = (e: React.TouchEvent) => {\n    if (disabled || !sliderRef.current) return;\n    e.stopPropagation();\n    const baseRatio = checked.get();\n    const clientX = e.touches[0].clientX;\n    const displacementX = clientX - initialPointerX.get();\n    const ratio = baseRatio + displacementX / TRAVEL;\n    const overflow = ratio < 0 ? -ratio : ratio > 1 ? ratio - 1 : 0;\n    const overflowSign = ratio < 0 ? -1 : 1;\n    const dampedOverflow = (overflowSign * overflow) / 22;\n    xDragRatio.set(Math.min(1, Math.max(0, ratio)) + dampedOverflow);\n  };\n\n  const handleClick = (e: React.MouseEvent) => {\n    if (disabled) return;\n    const x = e.clientX;\n    const initialX = initialPointerX.get();\n    const distance = x - initialX;\n    if (Math.abs(distance) < 4) {\n      const wasChecked = checked.get() > 0.5;\n      const shouldBeChecked = !wasChecked;\n      checked.set(shouldBeChecked ? 1 : 0);\n      handleStateChange(shouldBeChecked, wasChecked);\n    }\n  };\n\n  return (\n    <motion.div\n      ref={sliderRef}\n      className={cn(\n        \"touch-pan-y touch-none select-none\",\n        disabled && \"pointer-events-none opacity-50\",\n        className\n      )}\n      style={{\n        display: \"inline-block\",\n        width: sliderWidth,\n        height: sliderHeight,\n        backgroundColor: backgroundColor,\n        borderRadius: sliderHeight / 2,\n        position: \"relative\",\n        cursor: disabled ? \"not-allowed\" : \"pointer\",\n      }}\n      onMouseMove={handleMouseMove}\n      onTouchMove={handleTouchMove}\n      onClick={handleClick}\n    >\n      <RefractionFilter\n        id=\"thumb-filter\"\n        width={146}\n        height={92}\n        radius={46}\n        bezelWidth={19}\n        glassThickness={47}\n        bezelType=\"lip\"\n        refractiveIndex={1.5}\n        blur={blur}\n        scaleRatio={scaleRatio}\n        specularOpacity={specularOpacity}\n        specularSaturation={specularSaturation}\n      />\n      <motion.div\n        className=\"absolute\"\n        onTouchStart={(e) => {\n          if (disabled) return;\n          e.stopPropagation();\n          const pointerX = e.touches[0].clientX;\n          pointerDown.set(1);\n          initialPointerX.set(pointerX);\n        }}\n        onMouseDown={(e) => {\n          if (disabled) return;\n          e.stopPropagation();\n          pointerDown.set(1);\n          initialPointerX.set(e.clientX);\n        }}\n        style={{\n          height: thumbHeight,\n          width: thumbWidth,\n          marginLeft:\n            -THUMB_REST_OFFSET +\n            (sliderHeight - thumbHeight * THUMB_REST_SCALE) / 2,\n          x: useTransform(() => xRatio.get() * TRAVEL),\n          y: \"-50%\",\n          borderRadius: thumbRadius,\n          top: sliderHeight / 2,\n          backdropFilter: `url(#thumb-filter)`,\n          scale: thumbScale,\n          backgroundColor: useTransform(backgroundOpacity, (op) => {\n            // Apply opacity to thumb color\n            if (thumbBaseColor.startsWith(\"rgba\")) {\n              // Replace the alpha value\n              return thumbBaseColor.replace(/[\\d.]+\\)$/, `${op})`);\n            } else if (thumbBaseColor.startsWith(\"rgb\")) {\n              // Convert rgb to rgba\n              return thumbBaseColor\n                .replace(\"rgb\", \"rgba\")\n                .replace(\")\", `, ${op})`);\n            } else {\n              // For hex or named colors, just use white with opacity\n              return `rgba(255, 255, 255, ${op})`;\n            }\n          }),\n          boxShadow: useTransform(() => {\n            const isPressed = pointerDown.get() > 0.5;\n            return (\n              \"0 4px 22px rgba(0,0,0,0.1)\" +\n              (isPressed\n                ? \", inset 2px 7px 24px rgba(0,0,0,0.09), inset -2px -7px 24px rgba(255,255,255,0.09)\"\n                : \"\")\n            );\n          }),\n        }}\n      />\n    </motion.div>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "docs": "https://jyotimoydas.in/components/slide-to-unlock",
  "type": "registry:component"
}
