69 lines
2.1 KiB
TypeScript
69 lines
2.1 KiB
TypeScript
import { motion } from 'framer-motion';
|
|
import { Box } from './Box';
|
|
import { Text } from './Text';
|
|
|
|
interface ToggleProps {
|
|
checked: boolean;
|
|
onChange: (checked: boolean) => void;
|
|
label: string;
|
|
description?: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function Toggle({
|
|
checked,
|
|
onChange,
|
|
label,
|
|
description,
|
|
disabled = false,
|
|
}: ToggleProps) {
|
|
return (
|
|
<label className={`flex items-start justify-between cursor-pointer py-3 border-b border-charcoal-outline/50 last:border-b-0 ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}>
|
|
<Box style={{ flex: 1, paddingRight: '1rem' }}>
|
|
<Text weight="medium" color="text-gray-200" block>{label}</Text>
|
|
{description && (
|
|
<Text size="sm" color="text-gray-500" block mt={1}>{description}</Text>
|
|
)}
|
|
</Box>
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={checked}
|
|
onClick={() => !disabled && onChange(!checked)}
|
|
disabled={disabled}
|
|
className={`relative w-12 h-6 rounded-full transition-colors duration-200 flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-primary-blue/50 ${
|
|
checked
|
|
? 'bg-primary-blue'
|
|
: 'bg-iron-gray'
|
|
} ${disabled ? 'cursor-not-allowed' : ''}`}
|
|
>
|
|
{/* Glow effect when active */}
|
|
{checked && (
|
|
<motion.div
|
|
className="absolute inset-0 rounded-full bg-primary-blue"
|
|
initial={{ boxShadow: '0 0 0px rgba(25, 140, 255, 0)' }}
|
|
animate={{ boxShadow: '0 0 12px rgba(25, 140, 255, 0.4)' }}
|
|
transition={{ duration: 0.2 }}
|
|
/>
|
|
)}
|
|
|
|
{/* Knob */}
|
|
<motion.span
|
|
className="absolute top-0.5 w-5 h-5 bg-white rounded-full shadow-md"
|
|
initial={false}
|
|
animate={{
|
|
x: checked ? 24 : 2,
|
|
scale: 1,
|
|
}}
|
|
whileTap={{ scale: disabled ? 1 : 0.9 }}
|
|
transition={{
|
|
type: 'spring',
|
|
stiffness: 500,
|
|
damping: 30,
|
|
}}
|
|
/>
|
|
</button>
|
|
</label>
|
|
);
|
|
}
|