{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","categories":["effects","silly"],"dependencies":[],"description":"Fastest countdown ever","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { useState } from \"react\";\nimport { SnailTimer } from \"./snail-timer\";\n\nconst Preview = () => {\n  const [started, setStarted] = useState(false);\n\n  const handleToggleStarted = () => {\n    setStarted((prev) => !prev);\n  };\n\n  return (\n    <div className=\"flex h-screen items-center justify-center\">\n      <button type=\"button\" onClick={handleToggleStarted}>\n        {started ? \"Stop Timer\" : \"Start Countdown\"}\n      </button>\n      <SnailTimer started={started} />\n    </div>\n  );\n};\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/snail-timer/preview.tsx","type":"registry:file"},{"content":"/* CSS custom properties and animations for SnailTimer.\n   Scoped to the component root so a copy-pasted registry item cannot collide\n   with the host app's global custom properties. `--snail-move-duration` is\n   overridden per-instance by an inline style on `.snail-timer__track`. */\n.snail-timer {\n  --snail-move-duration: 45s;\n  --snail-eye-duration: 1s;\n  --snail-scale-duration: 1s;\n  --snail-dust-duration: 1s;\n}\n\n@keyframes snail-move {\n  from {\n    transform: translateX(100vw);\n  }\n  to {\n    transform: translateX(0);\n  }\n}\n\n@keyframes snail-scale {\n  0% {\n    transform: scaleX(1);\n  }\n  50% {\n    transform: scaleX(0.95);\n  }\n  100% {\n    transform: scaleX(1);\n  }\n}\n\n@keyframes snail-eye {\n  0% {\n    transform: translate(0);\n  }\n  50% {\n    transform: translate(3px, 0);\n  }\n  100% {\n    transform: translate(0);\n  }\n}\n\n@keyframes snail-dust {\n  100% {\n    background-position-x: right;\n  }\n}\n\n.snail-timer__track {\n  animation: snail-move var(--snail-move-duration) linear 1 both;\n}\n\n.snail .right-eye {\n  animation: snail-eye var(--snail-eye-duration) ease infinite;\n  animation-delay: 0.1s;\n}\n\n.snail .body {\n  animation: snail-scale var(--snail-scale-duration) ease infinite;\n  animation-delay: 0.1s;\n}\n\n.snail .shell {\n  animation: snail-scale var(--snail-scale-duration) ease infinite;\n}\n\n.snail-timer__dust {\n  width: 197px;\n  height: 66px;\n  background-image: url(\"https://d24l2zb4cwkekfpl.public.blob.vercel-storage.com/snail-timer/dust.svg\");\n  background-size: 5910px 67px;\n  animation: snail-dust var(--snail-dust-duration) steps(29) infinite forwards;\n  transform: translate(-20px, 10px);\n}\n\n/* Pause all animations when not started */\n.snail-timer.is-paused .snail-timer__track,\n.snail-timer.is-paused .snail .right-eye,\n.snail-timer.is-paused .snail .body,\n.snail-timer.is-paused .snail .shell,\n.snail-timer.is-paused .snail-timer__dust {\n  animation-play-state: paused !important;\n}\n","path":"/snail-timer.css","target":"uicapsule/snail-timer/snail-timer.css","type":"registry:file"},{"content":"import { useEffect, useState } from \"react\";\nimport type { CSSProperties } from \"react\";\n\nimport \"./snail-timer.css\";\n\ninterface Props {\n  started?: boolean;\n  initialSeconds?: number;\n  onTimeout?: () => void;\n}\n\n/** `CSSProperties` widened to accept custom properties without a type assertion. */\ninterface CSSVarProperties extends CSSProperties {\n  [key: `--${string}`]: string | number | undefined;\n}\n\n/** Module-level so the default prop keeps a stable identity across renders. */\nconst NOOP = () => {\n  /* empty */\n};\n\nconst SnailSvg = () => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"138\" height=\"67\" viewBox=\"0 0 138 67\">\n    <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(2 2)\">\n      <g className=\"left-eye\">\n        <g transform=\"translate(0 17.101)\">\n          <path\n            fill=\"#FFF\"\n            d=\"M22,11.3988978 C22,17.4738978 17.075,22.3988978 11,22.3988978 C4.925,22.3988978 0,17.4738978 0,11.3988978 C0,5.32389785 4.925,0.398897849 11,0.398897849 C17.075,0.398897849 22,5.32389785 22,11.3988978\"\n          />\n          <path\n            stroke=\"#7247FF\"\n            strokeWidth=\"4\"\n            d=\"M22,11.3988978 C22,17.4738978 17.075,22.3988978 11,22.3988978 C4.925,22.3988978 0,17.4738978 0,11.3988978 C0,5.32389785 4.925,0.398897849 11,0.398897849 C17.075,0.398897849 22,5.32389785 22,11.3988978 Z\"\n          />\n          <path\n            fill=\"#000\"\n            d=\"M12.5,12.8988978 C12.5,15.6608978 10.933,17.8988978 9,17.8988978 C7.067,17.8988978 5.5,15.6608978 5.5,12.8988978 C5.5,10.1368978 7.067,7.89889785 9,7.89889785 C10.933,7.89889785 12.5,10.1368978 12.5,12.8988978\"\n          />\n          <path\n            fill=\"#9A70EA\"\n            d=\"M11.231,10.9769978 L0.882,13.7729978 C0.46,12.2159978 0.377,10.5889978 0.638,9.00599785 C1.616,3.08099785 7.202,-0.904002151 13.014,0.176997849 C17.044,0.926997849 20.353,3.95999785 21.523,7.97899785 L11.231,10.9769978 Z\"\n          />\n          <path\n            stroke=\"#7247FF\"\n            strokeWidth=\"4\"\n            d=\"M11.231,10.9769978 L0.882,13.7729978 C0.46,12.2159978 0.377,10.5889978 0.638,9.00599785 C1.616,3.08099785 7.202,-0.904002151 13.014,0.176997849 C17.044,0.926997849 20.353,3.95999785 21.523,7.97899785 L11.231,10.9769978 Z\"\n          />\n          <path\n            fill=\"#7247FF\"\n            d=\"M15.8379,27.5687978 L12.0089,22.6057978 L19.4229,16.8857978 L23.2519,21.8487978 C24.8319,23.8957978 24.4519,26.8367978 22.4049,28.4157978 C20.3579,29.9957978 17.4179,29.6157978 15.8379,27.5687978\"\n          />\n        </g>\n      </g>\n      <g className=\"body\">\n        <g transform=\"translate(16.943 41.912)\">\n          <path\n            stroke=\"#7247FF\"\n            strokeWidth=\"5\"\n            d=\"M4.737,19.5 C2.121,19.5 8.52651283e-14,17.379 8.52651283e-14,14.763 C8.52651283e-14,7.161 6.161,4.54747351e-13 13.762,4.54747351e-13 L95.5,4.54747351e-13 C105.745,4.54747351e-13 113.106,7.242 116.407,15.936 C117.061,17.657 115.75,19.5 113.908,19.5 L4.737,19.5 Z\"\n          />\n          <path\n            fill=\"#9A70EA\"\n            d=\"M15.8429,19.2422 C13.4759,19.2422 11.5569,17.9202 11.5569,16.2892 C11.5569,11.5522 17.1319,7.0882 24.0079,7.0882 L97.9539,7.0882 C107.2219,7.0882 113.8819,11.6012 116.8679,17.0202 C117.4599,18.0942 116.2739,19.2422 114.6079,19.2422 L15.8429,19.2422 Z\"\n          />\n        </g>\n      </g>\n      <g className=\"right-eye\">\n        <g transform=\"translate(6 16.101)\">\n          <path\n            fill=\"#FFF\"\n            d=\"M22,11.3988978 C22,17.4738978 17.075,22.3988978 11,22.3988978 C4.925,22.3988978 0,17.4738978 0,11.3988978 C0,5.32389785 4.925,0.398897849 11,0.398897849 C17.075,0.398897849 22,5.32389785 22,11.3988978\"\n          />\n          <path\n            stroke=\"#7247FF\"\n            strokeWidth=\"4\"\n            d=\"M22,11.3988978 C22,17.4738978 17.075,22.3988978 11,22.3988978 C4.925,22.3988978 0,17.4738978 0,11.3988978 C0,5.32389785 4.925,0.398897849 11,0.398897849 C17.075,0.398897849 22,5.32389785 22,11.3988978 Z\"\n          />\n          <path\n            fill=\"#000\"\n            d=\"M12.5,12.8988978 C12.5,15.6608978 10.933,17.8988978 9,17.8988978 C7.067,17.8988978 5.5,15.6608978 5.5,12.8988978 C5.5,10.1368978 7.067,7.89889785 9,7.89889785 C10.933,7.89889785 12.5,10.1368978 12.5,12.8988978\"\n          />\n          <path\n            stroke=\"#7247FF\"\n            strokeWidth=\"4\"\n            d=\"M11.231,10.9769978 L0.882,13.7729978 C0.46,12.2159978 0.377,10.5889978 0.638,9.00599785 C1.616,3.08099785 7.202,-0.904002151 13.014,0.176997849 C17.044,0.926997849 20.353,3.95999785 21.523,7.97899785 L11.231,10.9769978 Z\"\n          />\n          <path\n            fill=\"#7247FF\"\n            d=\"M15.8379,27.5687978 L12.0089,22.6057978 L19.4229,16.8857978 L23.2519,21.8487978 C24.8319,23.8957978 24.4519,26.8367978 22.4049,28.4157978 C20.3579,29.9957978 17.4179,29.6157978 15.8379,27.5687978\"\n          />\n          <path\n            fill=\"#9A70EA\"\n            d=\"M11.231,10.9769978 L0.882,13.7729978 C0.46,12.2159978 0.377,10.5889978 0.638,9.00599785 C1.616,3.08099785 7.202,-0.904002151 13.014,0.176997849 C17.044,0.926997849 20.353,3.95999785 21.523,7.97899785 L11.231,10.9769978 Z\"\n          />\n        </g>\n      </g>\n      <g className=\"shell\">\n        <g strokeLinecap=\"round\" transform=\"translate(48.357 .18)\">\n          <path\n            stroke=\"#9A70EA\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\"\n            d=\"M16.0033626,9.63291273 C16.1763626,9.93291273 22.2683626,5.87991273 22.3263626,5.98291273 C22.3383626,6.00291273 0.761362599,23.8309127 1.1423626,24.4909127 C1.6783626,25.4199127 43.0693626,-0.781087271 43.5313626,0.0179127286 C44.0943626,0.992912729 3.7573626,30.0919127 4.0333626,30.5699127 C4.3803626,31.1699127 53.0443626,1.97991273 53.1703626,2.19991273 C53.2003626,2.25191273 -0.409637401,38.7729127 0.00236259944,39.4859127 C0.605362599,40.5299127 59.5263626,3.54891273 60.2073626,4.72691273 C60.7243626,5.62291273 11.9393626,37.8099127 12.6913626,39.1119127 C12.9233626,39.5149127 65.9643626,8.34791273 65.9673626,8.35391273 C66.3013626,8.93091273 22.6833626,38.4489127 23.0793626,39.1349127 C23.5083626,39.8779127 67.0573626,12.2429127 67.7073626,13.3679127 C68.1923626,14.2099127 26.5803626,42.7639127 27.2403626,43.9059127 C27.6733626,44.6569127 72.3033626,15.9939127 73.1243626,17.4149127 C73.8003626,18.5869127 28.5813626,47.7239127 29.2603626,48.8999127 C29.3993626,49.1389127 71.0653626,24.0769127 71.3623626,24.5919127 C71.9703626,25.6439127 27.7703626,56.1919127 27.9103626,56.4349127 C28.4413626,57.3559127 73.2133626,28.9199127 73.8013626,29.9399127 C74.3493626,30.8879127 33.3113626,58.8449127 33.6013626,59.3469127 C33.8473626,59.7729127 76.3823626,34.1119127 76.6143626,34.5139127 C76.8813626,34.9769127 37.8373626,62.5979127 38.2013626,63.2279127 C38.4243626,63.6129127 67.8783626,45.4039127 68.1763626,45.9209127\"\n          />\n          <path\n            stroke=\"#7247FF\"\n            strokeWidth=\"5\"\n            d=\"M1.6429626,39.3195127 C1.9699626,36.3825127 -0.857037401,22.8195127 16.6429626,9.31951273 C27.9219626,0.619512729 40.9809626,0.325512729 44.6429626,0.319480661 C49.6539626,0.311512729 55.7319626,1.79851273 63.1429626,6.81951273 C64.4599626,7.71251273 75.8079626,15.6825127 76.1429626,29.8195127 C76.4399626,42.3755127 67.8259626,50.3395127 65.1429626,52.8195127 C62.2099626,55.5315127 54.2939626,62.8485127 43.1429626,61.8195127 C35.5579626,61.1195127 24.3289626,56.3575127 22.1429626,45.8195127 C20.2179626,36.5385127 26.3509626,27.8065127 33.1429626,23.8195127 C37.8959626,21.0295127 42.6289626,20.7635127 45.1429626,20.8195127\"\n          />\n        </g>\n      </g>\n    </g>\n  </svg>\n);\n\nexport const SnailTimer = ({ started = true, initialSeconds = 45, onTimeout = NOOP }: Props) => {\n  const [seconds, setSeconds] = useState(initialSeconds);\n  const trackStyle: CSSVarProperties = { \"--snail-move-duration\": `${initialSeconds}s` };\n\n  useEffect(() => {\n    let interval: ReturnType<typeof setInterval> | undefined;\n\n    if (started) {\n      interval = setInterval(() => {\n        if (seconds === 1) {\n          onTimeout();\n          clearInterval(interval);\n        } else {\n          setSeconds((current) => current - 1);\n        }\n      }, 1000);\n    }\n\n    return () => clearInterval(interval);\n  }, [started, seconds, onTimeout]);\n\n  return (\n    <div\n      className={`snail-timer pointer-events-none absolute right-0 bottom-5 left-0 overflow-hidden ${\n        started ? \"\" : \"is-paused\"\n      }`}\n    >\n      <div className=\"snail-timer__track\" style={trackStyle}>\n        <div className=\"flex items-end\">\n          <div className=\"snail\">\n            <SnailSvg />\n          </div>\n          <div className=\"snail-timer__dust\" aria-hidden=\"true\" />\n        </div>\n        <div className=\"text-muted-foreground mt-1 ml-2 text-xs\">{seconds} seconds remaining</div>\n      </div>\n    </div>\n  );\n};\n","path":"/snail-timer.tsx","target":"uicapsule/snail-timer/snail-timer.tsx","type":"registry:file"},{"content":"declare module \"*.css\" {}\n","path":"/styles.d.ts","target":"uicapsule/snail-timer/styles.d.ts","type":"registry:file"}],"homepage":"https://www.uicapsule.com/ui/snail-timer","name":"snail-timer","registryDependencies":[],"title":"Snail Timer","type":"registry:block"}