{
    "id": "bhaskar/spring-toggle",
    "kind": "component",
    "title": "Spring Toggle",
    "summary": "An on/off switch whose knob stretches as it travels, then settles with a spring.",
    "author": "Admin",
    "stars": 0,
    "url": "https://ownlyplans.com/bhaskar/spring-toggle/",
    "stack": [],
    "tools": [
        "claude-code",
        "cursor",
        "lovable"
    ],
    "difficulty": "beginner",
    "license": "mit",
    "version": "1.0",
    "overview": "A real button with role=\"switch\" and aria-checked, so keyboards and screen readers work out of the box.",
    "steps": [],
    "files": [],
    "install": "npx shadcn@latest add https://ownlyplans.com/r/bhaskar/spring-toggle.json",
    "how_to_use": "Use the component code (html, css, js, or react) directly, or run the install command.",
    "component": {
        "category": "Toggles and inputs",
        "html": "<button class=\"st\" type=\"button\" role=\"switch\" aria-checked=\"false\" aria-label=\"Notifications\"><span class=\"st__k\"></span></button>",
        "css": ".st{position:relative;width:64px;height:36px;padding:0;border:0;border-radius:999px;background:color-mix(in srgb,var(--ink) 18%,transparent);cursor:pointer;transition:background .3s}\n.st[aria-checked=true]{background:var(--accent)}\n.st:focus-visible{outline:2px solid var(--accent);outline-offset:3px}\n.st__k{position:absolute;top:4px;left:4px;width:28px;height:28px;border-radius:999px;background:#fff;box-shadow:0 2px 6px rgba(0,0,0,.25);transition:left .45s cubic-bezier(.3,1.5,.5,1),width .2s}\n.st:active .st__k{width:34px}\n.st[aria-checked=true] .st__k{left:32px}\n.st[aria-checked=true]:active .st__k{left:26px}\n@media (prefers-reduced-motion:reduce){.st__k{transition:none}}",
        "js": "const s=document.querySelector('.st');s.addEventListener('click',()=>s.setAttribute('aria-checked',String(s.getAttribute('aria-checked')!=='true')));",
        "react": "\"use client\";\nimport { useState } from \"react\";\n\nconst css = `\n.st{position:relative;width:64px;height:36px;padding:0;border:0;border-radius:999px;background:rgba(127,127,127,.3);cursor:pointer;transition:background .3s}\n.st[aria-checked=true]{background:#1659f5}\n.st:focus-visible{outline:2px solid #1659f5;outline-offset:3px}\n.st__k{position:absolute;top:4px;left:4px;width:28px;height:28px;border-radius:999px;background:#fff;box-shadow:0 2px 6px rgba(0,0,0,.25);transition:left .45s cubic-bezier(.3,1.5,.5,1),width .2s}\n.st:active .st__k{width:34px}\n.st[aria-checked=true] .st__k{left:32px}\n.st[aria-checked=true]:active .st__k{left:26px}\n@media (prefers-reduced-motion:reduce){.st__k{transition:none}}`;\n\nexport function SpringToggle({ label, checked, defaultChecked = false, onChange }: { label: string; checked?: boolean; defaultChecked?: boolean; onChange?: (on: boolean) => void }) {\n  const [inner, setInner] = useState(defaultChecked);\n  const on = checked ?? inner;\n  const flip = () => { setInner(!on); onChange?.(!on); };\n  return (\n    <>\n      <style>{css}</style>\n      <button type=\"button\" role=\"switch\" aria-checked={on} aria-label={label} className=\"st\" onClick={flip}>\n        <span className=\"st__k\" />\n      </button>\n    </>\n  );\n}\n\nexport default SpringToggle;",
        "dependencies": [],
        "install": "npx shadcn@latest add https://ownlyplans.com/r/bhaskar/spring-toggle.json",
        "licence_notice": ""
    }
}