{
  "slug": "story-countdown",
  "title": "Story countdown 9:16",
  "tagline": "3-2-1 countdown with pulse rings, then a headline and a pill CTA. Vertical 1080×1920 for Stories, Reels, Shorts. 6 s.",
  "description": "Free CC0 vertical countdown motion template for Instagram Stories, Reels, TikTok and Shorts: numbers, pulse rings, headline, CTA pill, progress bar. Self-contained HTML, agent-friendly.",
  "category": "social",
  "tags": [
    "countdown",
    "story",
    "reels",
    "shorts",
    "vertical",
    "9:16"
  ],
  "tier": "free",
  "license": "CC0",
  "spec": {
    "width": 1080,
    "height": 1920,
    "fps": 30,
    "duration": 6,
    "aspect": "9:16"
  },
  "kind": "html",
  "params_schema": {
    "type": "object",
    "properties": {
      "from": {
        "type": "integer",
        "minimum": 1,
        "maximum": 5,
        "default": 3,
        "description": "Count from this number down to 1 (one second each)"
      },
      "goText": {
        "type": "string",
        "default": "GO",
        "maxLength": 8,
        "description": "Word shown after the countdown"
      },
      "headline": {
        "type": "string",
        "default": "New drop this Friday",
        "maxLength": 60,
        "description": "Two-line headline under the number"
      },
      "cta": {
        "type": "string",
        "default": "Set a reminder",
        "maxLength": 28,
        "description": "Pill button text"
      },
      "accent": {
        "type": "string",
        "format": "color",
        "default": "#ff4d6d",
        "description": "Rings, GO, pill, progress bar and the top glow"
      },
      "bg": {
        "type": "string",
        "format": "color",
        "default": "#0b0c10",
        "description": "Base background"
      }
    }
  },
  "default_params": {
    "from": 3,
    "goText": "GO",
    "headline": "New drop this Friday",
    "cta": "Set a reminder",
    "accent": "#ff4d6d",
    "bg": "#0b0c10"
  },
  "preview": {
    "poster": "https://gimgs.net/motion/media/story-countdown/poster-054b9d53d767.jpg",
    "video": ""
  },
  "variants": [],
  "credits": {
    "author": "gimgs",
    "url": "https://gimgs.net/motion"
  },
  "page_url": "https://gimgs.net/motion/story-countdown.html",
  "json_url": "https://gimgs.net/motion/story-countdown.json",
  "template_url": "https://gimgs.net/motion/story-countdown/template.html",
  "play_url": "https://gimgs.net/motion/story-countdown/play",
  "markdown_url": "https://gimgs.net/motion/story-countdown.md",
  "updated_at": "2026-09-24T07:24:39.232Z",
  "prompt": "## When to use\nTeasers and announcements for vertical social video: product drops, live-stream start, giveaway deadline, event reminder.\n\n## How to adapt\n- `from` = 3 gives the classic 3-2-1; the GO word appears at `from` seconds, headline at `from + 0.35`, CTA at `from + 0.7`. With `from: 5` the clip is fully used (6 s total); with `from: 1` there is a long hold — cut at ~3 s.\n- Keep `headline` under ~40 characters for two clean lines at 96px.\n- Match `accent` to the campaign colour; the top glow is derived from it automatically.\n- Add sound: a tick per second and a hit on GO line up with t = 0,1,2,… and t = `from`.\n\n## Do not change\n`window.motion`, `#stage` size (1080×1920), `animation-fill-mode: both`.",
  "flow": [
    {
      "step": "Write copy",
      "note": "headline ≤ 40 chars, cta ≤ 20 chars."
    },
    {
      "step": "Render 9:16",
      "cmd": "viewport 1080×1920; frames 0…179 at t=i/30 → ffmpeg -framerate 30 -i frame-%04d.png -c:v libx264 -pix_fmt yuv420p story.mp4"
    },
    {
      "step": "Add audio",
      "cmd": "ffmpeg -i story.mp4 -i ticks.wav -c:v copy -shortest story-audio.mp4"
    }
  ],
  "files": [],
  "contract": "Self-contained HTML. Declares window.motion = { width, height, fps, duration, params, apply(params) [, seek(t)] }; root element #stage sized width×height; animations are CSS/WAAPI with animation-fill-mode: both so any t can be seeked via document.getAnimations(). The host runtime (injected by /play) merges ?p params, calls apply(), scales #stage to the viewport and, when ?t is given, pauses and seeks deterministically.",
  "render": {
    "frames": 180,
    "frame_url": "https://gimgs.net/motion/story-countdown/play?p=<base64url(JSON params)>&t=<seconds>",
    "note": "Open frame_url in a headless browser with viewport width×height (deviceScaleFactor 1), wait for document.documentElement.dataset.motionReady === '1', screenshot; repeat for t = i/fps (i = 0…frames-1); then `ffmpeg -framerate <fps> -i frame-%04d.png -c:v libx264 -pix_fmt yuv420p out.mp4`. Or drop ?t to play it live."
  },
  "source": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Story countdown 9:16</title>\n<style>\n  @font-face{font-family:'Bricolage Grotesque';font-weight:600 800;font-display:block;src:url(https://gimgs.net/fonts/bricolage-600-latin.woff2) format('woff2')}\n  html,body{margin:0;background:#000}\n  #stage{position:relative;width:1080px;height:1920px;overflow:hidden;color:#fff;font-family:'Bricolage Grotesque',Inter,system-ui,sans-serif;\n    background:radial-gradient(120% 80% at 50% 0%,color-mix(in srgb,var(--accent,#ff4d6d) 55%,#000) 0%,var(--bg,#0b0c10) 60%)}\n  .num{position:absolute;left:50%;top:38%;transform:translate(-50%,-50%);font-weight:800;font-size:520px;line-height:1;letter-spacing:-.06em;\n    opacity:0;animation:num 1s var(--d) cubic-bezier(.2,.9,.2,1) both}\n  @keyframes num{0%{transform:translate(-50%,-50%) scale(1.6);opacity:0}25%{transform:translate(-50%,-50%) scale(1);opacity:1}80%{opacity:1}100%{transform:translate(-50%,-50%) scale(.9);opacity:0}}\n  .ring{position:absolute;left:50%;top:38%;width:640px;height:640px;border-radius:50%;border:6px solid var(--accent,#ff4d6d);transform:translate(-50%,-50%) scale(.6);opacity:0;\n    animation:ring 1s var(--d) cubic-bezier(.2,.9,.2,1) both}\n  /* 0% must be invisible: with fill-mode both the 0% state shows during the delay, so rings for 2 and 1 would stack on the 3 */\n  @keyframes ring{0%{transform:translate(-50%,-50%) scale(.6);opacity:0}8%{transform:translate(-50%,-50%) scale(.68);opacity:.9}100%{transform:translate(-50%,-50%) scale(1.5);opacity:0}}\n  .go{position:absolute;left:50%;top:38%;transform:translate(-50%,-50%);font-weight:800;font-size:260px;letter-spacing:-.04em;color:var(--accent,#ff4d6d);\n    opacity:0;animation:go .7s var(--go) cubic-bezier(.2,.9,.2,1.2) both}\n  @keyframes go{0%{transform:translate(-50%,-50%) scale(.4);opacity:0}100%{transform:translate(-50%,-50%) scale(1);opacity:1}}\n  .head{position:absolute;left:90px;right:90px;top:62%;text-align:center;font-weight:800;font-size:96px;line-height:1.05;letter-spacing:-.03em;\n    opacity:0;transform:translateY(40px);animation:up .6s calc(var(--go) + .35s) cubic-bezier(.2,.8,.2,1) both}\n  .cta{position:absolute;left:50%;top:80%;transform:translateX(-50%) translateY(30px);padding:26px 60px;border-radius:999px;background:var(--accent,#ff4d6d);color:#fff;font-weight:700;font-size:44px;letter-spacing:.02em;white-space:nowrap;\n    opacity:0;animation:cta .6s calc(var(--go) + .7s) cubic-bezier(.2,.8,.2,1) both}\n  @keyframes up{to{transform:none;opacity:1}}\n  @keyframes cta{to{transform:translateX(-50%);opacity:1}}\n  .bar{position:absolute;left:0;top:0;height:14px;background:var(--accent,#ff4d6d);width:0;animation:bar var(--total) linear both}\n  @keyframes bar{to{width:100%}}\n</style>\n</head>\n<body>\n<div id=\"stage\">\n  <div class=\"bar\"></div>\n  <div id=\"nums\"></div>\n  <div class=\"go\" id=\"go\"></div>\n  <div class=\"head\" id=\"head\"></div>\n  <div class=\"cta\" id=\"cta\"></div>\n</div>\n<script>\nwindow.motion = {\n  width: 1080, height: 1920, fps: 30, duration: 6,\n  params: { from: 3, goText: \"GO\", headline: \"New drop this Friday\", cta: \"Set a reminder\", accent: \"#ff4d6d\", bg: \"#0b0c10\" },\n  apply: function (p) {\n    var s = document.getElementById(\"stage\");\n    s.style.setProperty(\"--accent\", p.accent); s.style.setProperty(\"--bg\", p.bg);\n    var from = Math.max(1, Math.min(5, Math.round(+p.from || 3)));\n    var nums = document.getElementById(\"nums\");\n    while (nums.firstChild) nums.removeChild(nums.firstChild);\n    for (var i = 0; i < from; i++) {\n      var d = i * 1; // one second per number\n      var n = document.createElement(\"div\"); n.className = \"num\"; n.textContent = String(from - i); n.style.setProperty(\"--d\", d + \"s\"); nums.appendChild(n);\n      var r = document.createElement(\"div\"); r.className = \"ring\"; r.style.setProperty(\"--d\", d + \"s\"); nums.appendChild(r);\n    }\n    s.style.setProperty(\"--go\", from + \"s\");\n    s.style.setProperty(\"--total\", window.motion.duration + \"s\");\n    document.getElementById(\"go\").textContent = p.goText;\n    document.getElementById(\"head\").textContent = p.headline;\n    document.getElementById(\"cta\").textContent = p.cta;\n  }\n};\n</script>\n</body>\n</html>\n"
}