registry:component

Motion Switch

v1.0.0ga

Switch primitive with spring-based animated thumb.

Registry Endpoint
Open JSON

https://registry.aavya.com/r/ui-motion-switch.json

npx shadcn@latest add https://registry.aavya.com/r/ui-motion-switch.json

Live Preview
Usage Snippet
import MotionSwitch from '@/components/ui/motion-switch.tsx'

export default function Example() {
  return (
    <div className="p-6">
      <MotionSwitch />
    </div>
  )
}
Source Preview
'use client'

import * as React from 'react'

import * as SwitchPrimitive from '@radix-ui/react-switch'
import { motion } from 'motion/react'

import { cn } from '@/lib/utils'

const SIZES = {
  sm: { TRACK_WIDTH: 26, THUMB_SIZE: 14, THUMB_STRETCH: 18 },
  md: { TRACK_WIDTH: 32, THUMB_SIZE: 16, THUMB_STRETCH: 25 },
  lg: { TRACK_WIDTH: 48, THUMB_SIZE: 24, THUMB_STRETCH: 40 }
}

const STRETCH_DURATION = 120 // ms

type Size = keyof typeof SIZES

interface SwitchProps extends React.ComponentProps<typeof SwitchPrimitive.Root> {
  size?: Size
}

function Switch({ className, size = 'md', ...props }: SwitchProps) {
  const { TRACK_WIDTH, THUMB_SIZE, THUMB_STRETCH } = SIZES[size]
  const [isChecked, setIsChecked] = React.useState(props.checked ?? props.defaultChecked ?? false)
  const [isStretching, setIsStretching] = React.useState(false)

  React.useEffect(() => {
    if (props.checked !== undefined) setIsChecked(props.checked)
  }, [props.checked])

  React.useEffect(() => {
    setIsStretching(true)
    const timeout = setTimeout(() => setIsStretching(false), STRETCH_DURATION)

    return () => clearTimeout(timeout)
  }, [isChecked])

  const handleCheckedChange = (checked: boolean) => {
    setIsChecked(checked)
    props.onCheckedChange?.(checked)
  }

  const thumbWidth = isStretching ? THUMB_STRETCH : THUMB_SIZE
  const offsetUnchecked = 0
  const offsetChecked = TRACK_WIDTH -