{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"toggle-vault","type":"registry:ui","title":"Toggle Vault","description":"A lock switch styled as a vault door: the combination dial winds back, spins, and the bolts retract; locking spins it home and slams the bolts shut.","dependencies":["motion"],"registryDependencies":[],"files":[{"path":"registry/ui/toggle-vault.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useState } from \"react\";\nimport { motion, useAnimate, useReducedMotion } from \"motion/react\";\n\nexport interface ToggleVaultProps {\n  /** Controlled state. */\n  locked?: boolean;\n  defaultLocked?: boolean;\n  onChange?: (locked: boolean) => void;\n  /** Diameter in px. */\n  size?: number;\n  /** LED color when unlocked. */\n  openColor?: string;\n  /** LED color when locked. */\n  lockedColor?: string;\n  label?: string;\n  className?: string;\n}\n\nconst BOLTS = 6;\n\nexport function ToggleVault({\n  locked,\n  defaultLocked = true,\n  onChange,\n  size = 160,\n  openColor = \"#22c55e\",\n  lockedColor = \"#ef4444\",\n  label = \"Vault\",\n  className,\n}: ToggleVaultProps) {\n  const reduce = useReducedMotion();\n  const [inner, setInner] = useState(defaultLocked);\n  const isLocked = locked ?? inner;\n  const [door, animateDoor] = useAnimate();\n\n  const r = size * 0.38; // door radius\n  const boltLen = size * 0.13;\n  const boltW = size * 0.055;\n  const led = isLocked ? lockedColor : openColor;\n\n  const toggle = () => {\n    const next = !isLocked;\n    setInner(next);\n    onChange?.(next);\n    if (reduce) return;\n    // The \"clunk\": bolts slam home (lock) or the door breathes loose (unlock).\n    animateDoor(\n      door.current,\n      next ? { scale: [1, 0.96, 1.01, 1] } : { scale: [1, 1, 1.04, 1], rotate: [0, 0, -4, 0] },\n      { duration: next ? 0.35 : 0.5, delay: next ? 0.75 : 0.85 },\n    );\n  };\n\n  return (\n    <div className={`inline-flex flex-col items-center gap-4 ${className ?? \"\"}`}>\n      <button\n        type=\"button\"\n        role=\"switch\"\n        aria-checked={isLocked}\n        aria-label={`${label} lock`}\n        onClick={toggle}\n        className=\"group relative grid place-items-center rounded-full bg-muted shadow-[inset_0_6px_16px_rgb(0_0_0/.45),0_1px_0_rgb(255_255_255/.06)] outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background\"\n        style={{ width: size, height: size }}\n      >\n        {/* Bolts sit under the door; locked, they poke out into the frame. */}\n        {Array.from({ length: BOLTS }, (_, i) => (\n          <span\n            key={i}\n            aria-hidden\n            className=\"absolute top-1/2 left-1/2 size-0\"\n            style={{ rotate: `${(i * 360) / BOLTS + 30}deg` }}\n          >\n            <motion.span\n              className=\"absolute rounded-sm bg-[linear-gradient(90deg,#a1a1aa,#e4e4e7,#a1a1aa)] shadow\"\n              style={{ width: boltLen, height: boltW, top: -boltW / 2, left: r - boltLen * 0.55 }}\n              initial={false}\n              animate={{ x: isLocked ? 0 : -boltLen * 0.75 }}\n              transition={\n                reduce\n                  ? { duration: 0 }\n                  : isLocked\n                    ? { type: \"spring\", stiffness: 700, damping: 18, delay: 0.6 + i * 0.03 }\n                    : { type: \"spring\", stiffness: 300, damping: 26, delay: 0.55 + i * 0.05 }\n              }\n            />\n          </span>\n        ))}\n\n        <span\n          ref={door}\n          aria-hidden\n          className=\"relative grid place-items-center rounded-full border border-border bg-card shadow-[0_8px_20px_-6px_rgb(0_0_0/.6),inset_0_2px_0_rgb(255_255_255/.08)]\"\n          style={{ width: r * 2, height: r * 2 }}\n        >\n          {/* Rivets around the rim. */}\n          {Array.from({ length: 12 }, (_, i) => (\n            <span\n              key={i}\n              className=\"absolute size-1 rounded-full bg-muted-foreground/40\"\n              style={{ transform: `rotate(${i * 30}deg) translateY(${-r * 0.86}px)` }}\n            />\n          ))}\n          <span\n            className=\"absolute size-2 rounded-full transition-[background-color,box-shadow] duration-300\"\n            style={{ top: r * 0.3, right: r * 0.36, background: led, boxShadow: `0 0 8px 1px ${led}` }}\n          />\n          {/* Index mark the dial numbers line up against. */}\n          <span\n            className=\"absolute border-x-[5px] border-t-[7px] border-x-transparent\"\n            style={{ top: r * 0.22, borderTopColor: led }}\n          />\n          <motion.span\n            className=\"relative grid place-items-center rounded-full border border-border bg-[repeating-conic-gradient(from_-1deg,var(--muted-foreground)_0_2deg,transparent_2deg_15deg)] shadow-[0_4px_10px_-2px_rgb(0_0_0/.5)] group-active:scale-95\"\n            style={{ width: r * 1.2, height: r * 1.2 }}\n            initial={false}\n            animate={{ rotate: isLocked ? [270, 300, 0] : [0, -30, 270] }}\n            transition={reduce ? { duration: 0 } : { duration: 0.95, times: [0, 0.2, 1], ease: [\"easeOut\", [0.2, 0.9, 0.3, 1.15]] }}\n          >\n            <span className=\"absolute inset-[16%] rounded-full bg-card\" />\n            {/* Three-spoke handle with grips, reaching past the dial. */}\n            {[60, 180, 300].map((a) => (\n              <span\n                key={a}\n                className=\"absolute w-1.5 origin-bottom rounded-full bg-foreground/75\"\n                style={{ height: r * 0.72, bottom: \"50%\", rotate: `${a}deg` }}\n              >\n                <span\n                  className=\"absolute -top-1.5 left-1/2 -translate-x-1/2 rounded-full bg-foreground/90 shadow\"\n                  style={{ width: r * 0.16, height: r * 0.16 }}\n                />\n              </span>\n            ))}\n            <span className=\"relative size-[22%] rounded-full bg-foreground/80 shadow-inner\" />\n          </motion.span>\n        </span>\n      </button>\n      <span aria-live=\"polite\" className=\"flex items-center gap-2 text-sm font-medium text-foreground\">\n        <span className=\"size-2 rounded-full transition-colors duration-300\" style={{ background: led }} />\n        {isLocked ? \"Locked\" : \"Unlocked\"}\n      </span>\n    </div>\n  );\n}\n"}]}