{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shield-lock",
  "type": "registry:component",
  "title": "Shield Lock",
  "description": "A glass shield that fills with liquid as it checks a passkey – the padlock flips colour as the tide rises past it and snaps shut, while a bad key turns the water red and drains it away.",
  "categories": [
    "illustration",
    "Objects"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/shield-lock.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { LockIcon, LockOpenIcon, ScanLineIcon, ShieldXIcon } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  animate as animateValue,\n  motion,\n  useAnimate,\n  useInView,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ShieldLockHue = \"sky\" | \"emerald\" | \"amber\" | \"violet\" | \"rose\" | \"orange\" | \"indigo\" | \"teal\";\n\nconst hues: Record<ShieldLockHue, string> = {\n  sky: \"[--hue-hi:var(--color-sky-400)] [--hue:var(--color-sky-500)] [--hue-lo:var(--color-sky-600)] [--hue-deep:var(--color-sky-800)]\",\n  emerald:\n    \"[--hue-hi:var(--color-emerald-400)] [--hue:var(--color-emerald-500)] [--hue-lo:var(--color-emerald-600)] [--hue-deep:var(--color-emerald-800)]\",\n  amber:\n    \"[--hue-hi:var(--color-amber-400)] [--hue:var(--color-amber-500)] [--hue-lo:var(--color-amber-600)] [--hue-deep:var(--color-amber-800)]\",\n  violet:\n    \"[--hue-hi:var(--color-violet-400)] [--hue:var(--color-violet-500)] [--hue-lo:var(--color-violet-600)] [--hue-deep:var(--color-violet-800)]\",\n  rose: \"[--hue-hi:var(--color-rose-400)] [--hue:var(--color-rose-500)] [--hue-lo:var(--color-rose-600)] [--hue-deep:var(--color-rose-800)]\",\n  orange:\n    \"[--hue-hi:var(--color-orange-400)] [--hue:var(--color-orange-500)] [--hue-lo:var(--color-orange-600)] [--hue-deep:var(--color-orange-800)]\",\n  indigo:\n    \"[--hue-hi:var(--color-indigo-400)] [--hue:var(--color-indigo-500)] [--hue-lo:var(--color-indigo-600)] [--hue-deep:var(--color-indigo-800)]\",\n  teal: \"[--hue-hi:var(--color-teal-400)] [--hue:var(--color-teal-500)] [--hue-lo:var(--color-teal-600)] [--hue-deep:var(--color-teal-800)]\",\n};\n\nexport type ShieldLockStatus = \"open\" | \"scanning\" | \"locked\" | \"denied\";\n\nexport interface ShieldLockProps {\n  status?: ShieldLockStatus;\n  onRequest?: (next: \"locked\" | \"open\") => void;\n  hue?: ShieldLockHue;\n  labels?: Partial<Record<ShieldLockStatus, string>>;\n  className?: string;\n}\n\nconst defaultLabels: Record<ShieldLockStatus, string> = {\n  open: \"Vault unlocked\",\n  scanning: \"Checking passkey…\",\n  locked: \"Vault locked\",\n  denied: \"Passkey not recognised\",\n};\n\nconst icons = { open: LockOpenIcon, scanning: ScanLineIcon, locked: LockIcon, denied: ShieldXIcon };\n\nconst script: { status: ShieldLockStatus; ms: number }[] = [\n  { status: \"open\", ms: 1400 },\n  { status: \"scanning\", ms: 1300 },\n  { status: \"locked\", ms: 2800 },\n  { status: \"open\", ms: 1400 },\n  { status: \"scanning\", ms: 1300 },\n  { status: \"denied\", ms: 1700 },\n];\n\nconst W = 400;\nconst H = 300;\nconst SHIELD =\n  \"M200 34 C 230 50 262 58 292 58 V 146 C 292 206 254 246 200 270 C 146 246 108 206 108 146 V 58 C 138 58 170 50 200 34 Z\";\nconst INNER =\n  \"M200 52 C 226 65 254 72 278 72 V 146 C 278 197 246 231 200 252 C 154 231 122 197 122 146 V 72 C 146 72 174 65 200 52 Z\";\n\nconst WAVE = `M0 0 ${\"q12.5 -5 25 0 q12.5 5 25 0 \".repeat(9)}V 320 H 0 Z`;\n\nconst levels: Record<ShieldLockStatus, number> = { open: 0.2, scanning: 0.86, locked: 0.92, denied: 0.2 };\n\nfunction Padlock({\n  shut,\n  denied,\n  reduce,\n  round,\n  tone,\n}: { shut: boolean; denied: boolean; reduce: boolean; round: number; tone: \"dry\" | \"wet\" }) {\n  const wet = tone === \"wet\";\n  return (\n    <motion.g\n      animate={shut && !reduce ? { scale: [1, 0.95, 1.02, 1] } : { scale: 1 }}\n      key={`lock-${round}`}\n      style={{ originX: \"200px\", originY: \"170px\", transformBox: \"view-box\" }}\n      transition={{ delay: 0.1, duration: 0.42, ease: [0.23, 1, 0.32, 1] }}\n    >\n      <motion.path\n        animate={{ y: shut ? 0 : -13 }}\n        className={cn(\"fill-none\", wet ? \"stroke-white\" : \"stroke-(--hue)\")}\n        d=\"M182 156 V 134 a 18 18 0 0 1 36 0 V 156\"\n        initial={false}\n        strokeLinecap=\"round\"\n        strokeWidth={9}\n        transition={\n          reduce\n            ? { duration: 0 }\n            : shut\n              ? { type: \"spring\", stiffness: 900, damping: 26, mass: 0.6 }\n              : { type: \"spring\", stiffness: 260, damping: 22 }\n        }\n      />\n      {wet && <rect className=\"fill-black/15\" height={52} rx={12} width={72} x={164} y={153} />}\n      <rect className={wet ? \"fill-white\" : \"fill-(--hue)\"} height={52} rx={12} width={72} x={164} y={150} />\n      <g\n        className={cn(\n          \"transition-[fill] duration-300\",\n          wet ? (denied ? \"fill-destructive\" : \"fill-(--hue-deep)\") : \"fill-(--shell)\",\n        )}\n      >\n        <circle cx={200} cy={171} r={7} />\n        <rect height={16} rx={2.5} width={5} x={197.5} y={172} />\n      </g>\n    </motion.g>\n  );\n}\n\nfunction useCalmMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nexport function ShieldLock({ status, onRequest, hue = \"sky\", labels, className }: ShieldLockProps) {\n  const uid = useId().replace(/:/g, \"\");\n  const reduce = useCalmMotion();\n  const copy = { ...defaultLabels, ...labels };\n  const [demo, setDemo] = useState(status === undefined);\n  const [step, setStep] = useState(0);\n  const [local, setLocal] = useState<ShieldLockStatus>(\"open\");\n  const current: ShieldLockStatus = status ?? (demo ? script[step].status : local);\n  const [scope, animate] = useAnimate<HTMLDivElement>();\n  const [round, setRound] = useState(0);\n  const prev = useRef(current);\n  const root = useRef<HTMLDivElement>(null);\n  const inView = useInView(root);\n  const live = inView && !reduce;\n  const level = useMotionValue(levels[current]);\n  const surface = useTransform(level, (l) => 262 - l * 222);\n\n  useEffect(() => {\n    const target = levels[current];\n    if (reduce) {\n      level.set(target);\n      return;\n    }\n    const c =\n      current === \"scanning\"\n        ? animateValue(level, target, { duration: 1.2, ease: [0.65, 0, 0.35, 1] })\n        : current === \"denied\"\n          ? animateValue(level, target, { delay: 0.35, duration: 0.9, ease: [0.65, 0, 0.35, 1] })\n          : animateValue(level, target, { type: \"spring\", stiffness: 140, damping: 20 });\n    return () => c.stop();\n  }, [current, reduce, level]);\n\n  useEffect(() => {\n    if (!demo || status !== undefined || !inView) return;\n    const t = setTimeout(() => setStep((s) => (s + 1) % script.length), script[step].ms);\n    return () => clearTimeout(t);\n  }, [demo, status, step, inView]);\n\n  useEffect(() => {\n    if (demo || status !== undefined || local !== \"scanning\") return;\n    const t = setTimeout(() => setLocal(\"locked\"), 1300);\n    return () => clearTimeout(t);\n  }, [demo, status, local]);\n\n  useEffect(() => {\n    if (prev.current === current) return;\n    prev.current = current;\n    if (current === \"locked\" || current === \"denied\") setRound((r) => r + 1);\n    if (current === \"denied\" && !reduce && scope.current) {\n      animate(scope.current, { x: [0, -9, 8, -6, 4, -2, 0] }, { duration: 0.5, ease: \"easeOut\" });\n    }\n  }, [current, reduce, animate, scope]);\n\n  const click = () => {\n    const next = current === \"locked\" ? \"open\" : \"locked\";\n    onRequest?.(next);\n    if (status !== undefined) return;\n    setDemo(false);\n    setLocal(next === \"locked\" ? \"scanning\" : \"open\");\n  };\n\n  const shut = current === \"locked\";\n  const denied = current === \"denied\";\n  const Icon = icons[current];\n\n  return (\n    <div\n      ref={root}\n      className={cn(\n        \"relative w-full max-w-[440px] overflow-hidden rounded-2xl border bg-card p-4 shadow-xs/5 sm:p-6\",\n        \"[--rim:color-mix(in_oklab,var(--hue)_30%,var(--color-card))] [--shell:color-mix(in_oklab,var(--hue)_10%,var(--color-card))]\",\n        hues[hue],\n        className,\n      )}\n    >\n      <div\n        aria-hidden\n        className=\"dot-grid pointer-events-none absolute inset-0 opacity-50 [mask-image:radial-gradient(closest-side,black,transparent)]\"\n      />\n\n      <button\n        aria-label={shut ? \"Unlock the vault\" : \"Lock the vault\"}\n        className=\"group/shield relative mx-auto block aspect-[4/3] w-full max-w-[400px] cursor-pointer rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        onClick={click}\n        type=\"button\"\n      >\n        <motion.div\n          animate={{ opacity: shut ? 0.32 : 0.16, scale: shut ? 1.08 : 1 }}\n          aria-hidden\n          className={cn(\n            \"absolute top-1/2 left-1/2 size-1/2 -translate-1/2 rounded-full blur-3xl transition-colors duration-300\",\n            denied ? \"bg-destructive\" : \"bg-(--hue)\",\n          )}\n          initial={false}\n          transition={{ duration: 0.6, ease: [0.23, 1, 0.32, 1] }}\n        />\n        <div className=\"absolute inset-0 transition-transform duration-150 ease-out group-active/shield:scale-[0.98]\" ref={scope}>\n          <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\" viewBox={`0 0 ${W} ${H}`}>\n            <defs>\n              <linearGradient id={`${uid}-body`} x1=\"0\" x2=\"0.4\" y1=\"0\" y2=\"1\">\n                <stop offset=\"0\" stopColor=\"var(--hue-hi)\" />\n                <stop offset=\"1\" stopColor=\"var(--hue-lo)\" />\n              </linearGradient>\n              <clipPath id={`${uid}-inner`}>\n                <path d={INNER} />\n              </clipPath>\n              <mask height={H} id={`${uid}-wet`} maskUnits=\"userSpaceOnUse\" width={W} x={0} y={0}>\n                <motion.g style={{ y: surface }}>\n                  <motion.path\n                    animate={live ? { x: [0, -50] } : { x: 0 }}\n                    className=\"fill-white\"\n                    d={WAVE}\n                    initial={false}\n                    transition={live ? { duration: 2.2, ease: \"linear\", repeat: Number.POSITIVE_INFINITY } : { duration: 0 }}\n                  />\n                </motion.g>\n              </mask>\n            </defs>\n\n            <AnimatePresence>\n              {shut && !reduce && (\n                <motion.g key={`ring-${round}`}>\n                  {[0, 1].map((r) => (\n                    <motion.path\n                      animate={{ opacity: [0, 0.5, 0], scale: [1, 1.22] }}\n                      className=\"fill-none stroke-(--hue)\"\n                      d={SHIELD}\n                      initial={{ opacity: 0, scale: 1 }}\n                      key={r}\n                      strokeWidth={1.5}\n                      style={{ originX: \"50%\", originY: \"50%\" }}\n                      transition={{ delay: 0.12 + r * 0.22, duration: 1.1, ease: [0.23, 1, 0.32, 1] }}\n                    />\n                  ))}\n                </motion.g>\n              )}\n            </AnimatePresence>\n\n            <path className=\"fill-black/10\" d={SHIELD} transform=\"translate(0 6)\" />\n            <path className=\"fill-(--shell) stroke-(--rim)\" d={SHIELD} strokeWidth={1.5} />\n\n            <g clipPath={`url(#${uid}-inner)`}>\n              <motion.g style={{ y: surface }}>\n                <motion.path\n                  animate={live ? { x: [-50, 0] } : { x: -25 }}\n                  className={cn(\"opacity-45 transition-[fill] duration-300\", denied ? \"fill-destructive\" : \"fill-(--hue-hi)\")}\n                  d={WAVE}\n                  initial={false}\n                  transition={live ? { duration: 3.2, ease: \"linear\", repeat: Number.POSITIVE_INFINITY } : { duration: 0 }}\n                />\n                <motion.path\n                  animate={live ? { x: [0, -50] } : { x: 0 }}\n                  d={WAVE}\n                  fill={`url(#${uid}-body)`}\n                  initial={false}\n                  transition={live ? { duration: 2.2, ease: \"linear\", repeat: Number.POSITIVE_INFINITY } : { duration: 0 }}\n                />\n                <motion.path\n                  animate={{ opacity: denied ? 0.9 : 0, ...(live ? { x: [0, -50] } : { x: 0 }) }}\n                  className=\"fill-destructive\"\n                  d={WAVE}\n                  initial={false}\n                  transition={{\n                    opacity: { duration: denied ? 0.12 : 0.3 },\n                    x: live ? { duration: 2.2, ease: \"linear\", repeat: Number.POSITIVE_INFINITY } : { duration: 0 },\n                  }}\n                />\n              </motion.g>\n            </g>\n            <path className=\"fill-white/10 dark:fill-white/[0.05]\" d=\"M200 34 C 170 50 138 58 108 58 V 146 C 108 206 146 246 200 270 Z\" />\n\n            <Padlock denied={denied} reduce={reduce} round={round} shut={shut} tone=\"dry\" />\n            <g mask={`url(#${uid}-wet)`}>\n              <Padlock denied={denied} reduce={reduce} round={round} shut={shut} tone=\"wet\" />\n            </g>\n\n            <AnimatePresence>\n              {shut && !reduce && (\n                <motion.g className=\"stroke-(--hue)\" key={`spark-${round}`} strokeLinecap=\"round\" strokeWidth={2.5}>\n                  {[-60, -25, 25, 60].map((a) => {\n                    const rad = ((a - 90) * Math.PI) / 180;\n                    const x = 200 + Math.cos(rad) * 52;\n                    const y = 126 + Math.sin(rad) * 30;\n                    return (\n                      <motion.line\n                        animate={{ opacity: [0, 1, 0], pathLength: [0, 1, 1], x: [0, Math.cos(rad) * 8], y: [0, Math.sin(rad) * 8] }}\n                        exit={{ opacity: 0 }}\n                        initial={{ opacity: 0 }}\n                        key={a}\n                        transition={{ delay: 0.08, duration: 0.45, ease: \"easeOut\" }}\n                        x1={x}\n                        x2={x + Math.cos(rad) * 9}\n                        y1={y}\n                        y2={y + Math.sin(rad) * 9}\n                      />\n                    );\n                  })}\n                </motion.g>\n              )}\n            </AnimatePresence>\n          </svg>\n        </div>\n      </button>\n\n      <div aria-live=\"polite\" className=\"relative mt-1 flex h-6 items-center justify-center\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.div\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            className={cn(\n              \"flex items-center gap-1.5 font-medium text-sm\",\n              denied ? \"text-destructive-foreground\" : shut ? \"text-foreground\" : \"text-muted-foreground\",\n            )}\n            exit={{ opacity: 0, y: reduce ? 0 : -6, filter: reduce ? \"blur(0px)\" : \"blur(2px)\" }}\n            initial={{ opacity: 0, y: reduce ? 0 : 6, filter: reduce ? \"blur(0px)\" : \"blur(2px)\" }}\n            key={current}\n            transition={{ duration: 0.2, ease: [0.23, 1, 0.32, 1] }}\n          >\n            <Icon aria-hidden className=\"size-4\" />\n            {copy[current]}\n          </motion.div>\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}