{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elastic-switch",
  "title": "Elastic Switch",
  "description": "Toggle whose knob stretches while pressed and springs across with overshoot, with optional sun / moon icons.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/elastic-switch/elastic-switch.tsx",
      "content": "\"use client\"\n\nimport { useId, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface ElasticSwitchProps {\n  /** Controlled state. */\n  checked?: boolean\n  /** Initial state when uncontrolled. Default: false */\n  defaultChecked?: boolean\n  /** Called with the new state when toggled. */\n  onCheckedChange?: (checked: boolean) => void\n  /** Switch size. Default: \"md\" */\n  size?: \"sm\" | \"md\" | \"lg\"\n  /** Track color when on (any CSS color). Default: \"#ff6a2b\" */\n  onColor?: string\n  /** Track color when off (any CSS color). Default: a muted theme color */\n  offColor?: string\n  /** Show a sun / moon icon inside the knob. Default: false */\n  icons?: boolean\n  /** Visible label next to the switch. */\n  label?: string\n  /** Disable interaction. Default: false */\n  disabled?: boolean\n  /** Accessible label when no visible `label` is given. */\n  \"aria-label\"?: string\n  className?: string\n}\n\nconst sizes = {\n  sm: { w: 36, h: 20, knob: 16, pad: 2, icon: 10 },\n  md: { w: 50, h: 28, knob: 22, pad: 3, icon: 13 },\n  lg: { w: 64, h: 36, knob: 30, pad: 3, icon: 17 },\n}\n\nconst Sun = ({ size }: { size: number }) => (\n  <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.4} strokeLinecap=\"round\">\n    <circle cx=\"12\" cy=\"12\" r=\"4\" />\n    <path d=\"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\" />\n  </svg>\n)\n\nconst Moon = ({ size }: { size: number }) => (\n  <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.4} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <path d=\"M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5Z\" />\n  </svg>\n)\n\nexport function ElasticSwitch({\n  checked,\n  defaultChecked = false,\n  onCheckedChange,\n  size = \"md\",\n  onColor = \"#ff6a2b\",\n  offColor,\n  icons = false,\n  label,\n  disabled = false,\n  \"aria-label\": ariaLabel,\n  className,\n}: ElasticSwitchProps) {\n  const reduced = useReducedMotion()\n  const id = useId()\n  const [internal, setInternal] = useState(defaultChecked)\n  const [pressed, setPressed] = useState(false)\n  const on = checked ?? internal\n  const s = sizes[size]\n\n  const toggle = () => {\n    if (disabled) return\n    if (checked === undefined) setInternal(!on)\n    onCheckedChange?.(!on)\n  }\n\n  const knobW = pressed && !reduced ? s.knob * 1.35 : s.knob\n  const x = on ? s.w - s.pad * 2 - knobW : 0\n  const spring = reduced\n    ? { duration: 0 }\n    : { type: \"spring\" as const, stiffness: 520, damping: pressed ? 30 : 17, mass: 0.8 }\n\n  return (\n    <span className={cn(\"inline-flex items-center gap-3\", disabled && \"opacity-50\", className)}>\n      <button\n        id={id}\n        type=\"button\"\n        role=\"switch\"\n        aria-checked={on}\n        aria-label={label ? undefined : ariaLabel}\n        disabled={disabled}\n        onClick={toggle}\n        onPointerDown={() => setPressed(true)}\n        onPointerUp={() => setPressed(false)}\n        onPointerLeave={() => setPressed(false)}\n        onPointerCancel={() => setPressed(false)}\n        onKeyDown={(e) => e.key === \" \" && setPressed(true)}\n        onKeyUp={() => setPressed(false)}\n        onBlur={() => setPressed(false)}\n        className={cn(\n          \"relative shrink-0 cursor-pointer overflow-hidden rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed\",\n          !offColor && \"bg-muted-foreground/30\"\n        )}\n        style={{ width: s.w, height: s.h, padding: s.pad, background: offColor }}\n      >\n        <motion.span\n          aria-hidden=\"true\"\n          className=\"absolute inset-0\"\n          style={{ background: onColor }}\n          initial={false}\n          animate={{ opacity: on ? 1 : 0 }}\n          transition={{ duration: reduced ? 0 : 0.2 }}\n        />\n        <motion.span\n          aria-hidden=\"true\"\n          className=\"relative grid place-items-center overflow-hidden rounded-full bg-white shadow-[0_1px_3px_rgb(0_0_0/0.25)]\"\n          style={{ height: s.knob }}\n          initial={false}\n          animate={{ x, width: knobW }}\n          transition={spring}\n        >\n          {icons && (\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              <motion.span\n                key={on ? \"moon\" : \"sun\"}\n                className=\"grid place-items-center\"\n                style={{ color: on ? onColor : \"#a3a3a3\" }}\n                initial={reduced ? { opacity: 0 } : { opacity: 0, rotate: -90, scale: 0.4 }}\n                animate={{ opacity: 1, rotate: 0, scale: 1 }}\n                exit={reduced ? { opacity: 0 } : { opacity: 0, rotate: 90, scale: 0.4 }}\n                transition={reduced ? { duration: 0 } : { type: \"spring\", stiffness: 400, damping: 22 }}\n              >\n                {on ? <Moon size={s.icon} /> : <Sun size={s.icon} />}\n              </motion.span>\n            </AnimatePresence>\n          )}\n        </motion.span>\n      </button>\n      {label && (\n        <label htmlFor={id} className={cn(\"select-none text-sm font-medium text-foreground\", !disabled && \"cursor-pointer\")}>\n          {label}\n        </label>\n      )}\n    </span>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/elastic-switch",
  "type": "registry:component"
}