{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-toggle",
  "type": "registry:ui",
  "title": "Animated Toggle",
  "description": "A tactile switch whose thumb springs across the track and squashes when pressed.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/animated-toggle.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\ntype ToggleSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface AnimatedToggleProps\n  extends Omit<\n    React.ComponentPropsWithoutRef<\"button\">,\n    \"onChange\" | \"value\" | \"defaultValue\" | \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\" | \"onAnimationEnd\"\n  > {\n  /** Controlled state. */\n  checked?: boolean;\n  /** Initial state when uncontrolled. */\n  defaultChecked?: boolean;\n  /** Called with the next state whenever the switch is toggled. */\n  onCheckedChange?: (checked: boolean) => void;\n  /** Visible label, associated with the switch via <label htmlFor>. Use aria-label when omitted. */\n  label?: React.ReactNode;\n  /** Optional helper text, linked with aria-describedby. */\n  description?: React.ReactNode;\n  /** Which side of the switch the label sits on. */\n  labelPosition?: \"start\" | \"end\";\n  size?: ToggleSize;\n  /** Track colour when on (any CSS colour). */\n  accent?: string;\n  /** Show a small check / cross glyph inside the thumb. */\n  icons?: boolean;\n  /** Spring for the thumb's travel and squash. */\n  spring?: { stiffness: number; damping: number; mass: number };\n  /** When set, a hidden input submits \"on\" with forms while checked. */\n  name?: string;\n}\n\nconst SIZES: Record<ToggleSize, { w: number; h: number; pad: number; text: string }> = {\n  sm: { w: 34, h: 20, pad: 2, text: \"text-[13px]\" },\n  md: { w: 44, h: 26, pad: 3, text: \"text-sm\" },\n  lg: { w: 56, h: 32, pad: 3, text: \"text-base\" },\n};\n\n/**\n * A tactile switch: a real <button role=\"switch\"> whose thumb springs across the track and\n * squashes in the direction of travel while pressed. Supports controlled and uncontrolled use.\n */\nexport const AnimatedToggle = React.forwardRef<HTMLButtonElement, AnimatedToggleProps>(function AnimatedToggle(\n  {\n    checked: checkedProp,\n    defaultChecked = false,\n    onCheckedChange,\n    label,\n    description,\n    labelPosition = \"end\",\n    size = \"md\",\n    accent = \"#FF7448\",\n    icons = false,\n    spring = { stiffness: 520, damping: 30, mass: 0.8 },\n    name,\n    disabled,\n    id: idProp,\n    className = \"\",\n    style,\n    onClick,\n    ...props\n  },\n  ref,\n) {\n  const reduceMotion = useReducedMotion();\n  const autoId = React.useId();\n  const id = idProp ?? `toggle-${autoId}`;\n  const descriptionId = `${id}-description`;\n\n  const [uncontrolled, setUncontrolled] = React.useState(defaultChecked);\n  const isControlled = checkedProp !== undefined;\n  const checked = isControlled ? checkedProp : uncontrolled;\n\n  const [pressed, setPressed] = React.useState(false);\n\n  const dims = SIZES[size];\n  const thumb = dims.h - dims.pad * 2;\n  const travel = dims.w - thumb - dims.pad * 2;\n\n  const toggle = (event: React.MouseEvent<HTMLButtonElement>) => {\n    onClick?.(event);\n    if (event.defaultPrevented || disabled) return;\n    const next = !checked;\n    if (!isControlled) setUncontrolled(next);\n    onCheckedChange?.(next);\n  };\n\n  const transition = reduceMotion ? { duration: 0 } : { type: \"spring\" as const, ...spring };\n  // Squash: the thumb stretches toward the side it is about to travel to.\n  const squash = pressed && !disabled && !reduceMotion;\n\n  const control = (\n    <button\n      {...props}\n      ref={ref}\n      id={id}\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={checked}\n      aria-describedby={description ? descriptionId : props[\"aria-describedby\"]}\n      disabled={disabled}\n      onClick={toggle}\n      onPointerDown={(e) => {\n        props.onPointerDown?.(e);\n        if (e.button === 0) setPressed(true);\n      }}\n      onPointerUp={(e) => {\n        props.onPointerUp?.(e);\n        setPressed(false);\n      }}\n      onPointerLeave={(e) => {\n        props.onPointerLeave?.(e);\n        setPressed(false);\n      }}\n      onPointerCancel={(e) => {\n        props.onPointerCancel?.(e);\n        setPressed(false);\n      }}\n      onKeyDown={(e) => {\n        props.onKeyDown?.(e);\n        if (e.key === \" \" || e.key === \"Enter\") setPressed(true);\n      }}\n      onKeyUp={(e) => {\n        props.onKeyUp?.(e);\n        setPressed(false);\n      }}\n      onBlur={(e) => {\n        props.onBlur?.(e);\n        setPressed(false);\n      }}\n      className={`group/toggle relative inline-flex shrink-0 cursor-pointer items-center rounded-full border transition-[background-color,border-color,box-shadow] duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--tg-accent)] disabled:cursor-not-allowed disabled:opacity-45 motion-reduce:transition-none ${\n        checked\n          ? \"border-transparent bg-[var(--tg-accent)]\"\n          : \"border-zinc-900/10 bg-zinc-900/[0.08] hover:bg-zinc-900/[0.12] dark:border-white/10 dark:bg-white/[0.08] dark:hover:bg-white/[0.12]\"\n      }`}\n      style={{ width: dims.w, height: dims.h, padding: dims.pad - 1 }}\n    >\n      <motion.span\n        aria-hidden=\"true\"\n        className=\"relative grid place-items-center rounded-full bg-white shadow-[0_1px_2px_rgba(0,0,0,0.18),0_2px_6px_-1px_rgba(0,0,0,0.14)] dark:bg-zinc-50\"\n        style={{ width: thumb, height: thumb, originX: checked ? 1 : 0 }}\n        initial={false}\n        animate={{\n          x: checked ? travel : 0,\n          scaleX: squash ? 1.22 : 1,\n          scaleY: squash ? 0.9 : 1,\n        }}\n        transition={transition}\n      >\n        {icons && (\n          <svg viewBox=\"0 0 12 12\" className=\"size-[55%]\" fill=\"none\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n            <motion.path\n              d=\"M2.5 6.3 5 8.6 9.5 3.6\"\n              stroke=\"var(--tg-accent)\"\n              initial={false}\n              animate={{ pathLength: checked ? 1 : 0, opacity: checked ? 1 : 0 }}\n              transition={reduceMotion ? { duration: 0 } : { duration: 0.22, delay: checked ? 0.06 : 0 }}\n            />\n            <motion.path\n              d=\"M3.5 3.5 8.5 8.5M8.5 3.5 3.5 8.5\"\n              className=\"stroke-zinc-400\"\n              initial={false}\n              animate={{ pathLength: checked ? 0 : 1, opacity: checked ? 0 : 1 }}\n              transition={reduceMotion ? { duration: 0 } : { duration: 0.2 }}\n            />\n          </svg>\n        )}\n      </motion.span>\n    </button>\n  );\n\n  // Mirrors checkbox semantics: the field is only submitted while on.\n  const hiddenInput = name && checked ? <input type=\"hidden\" name={name} value=\"on\" /> : null;\n\n  if (!label && !description) {\n    return (\n      <span\n        className={`inline-flex ${className}`}\n        style={{ [\"--tg-accent\" as string]: accent, ...style }}\n      >\n        {control}\n        {hiddenInput}\n      </span>\n    );\n  }\n\n  return (\n    <span\n      className={`inline-flex items-start gap-3 ${labelPosition === \"start\" ? \"flex-row-reverse\" : \"\"} ${className}`}\n      style={{ [\"--tg-accent\" as string]: accent, ...style }}\n    >\n      {control}\n      <span className=\"flex min-w-0 flex-col\" style={{ paddingTop: Math.max(0, (dims.h - 20) / 2) }}>\n        {label && (\n          <label\n            htmlFor={id}\n            className={`select-none font-medium leading-5 tracking-tight text-zinc-900 dark:text-zinc-100 ${dims.text} ${\n              disabled ? \"cursor-not-allowed opacity-50\" : \"cursor-pointer\"\n            }`}\n          >\n            {label}\n          </label>\n        )}\n        {description && (\n          <span id={descriptionId} className={`mt-0.5 text-[13px] leading-snug text-zinc-500 dark:text-zinc-400 ${disabled ? \"opacity-60\" : \"\"}`}>\n            {description}\n          </span>\n        )}\n      </span>\n      {hiddenInput}\n    </span>\n  );\n});\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "docs": "Documentation: https://wancraft.my/components/animated-toggle"
}
