{
  "slug": "pill-chip-slot-cycle-handled",
  "title": "Pill chip slot cycle",
  "tagline": "In \"Your `chip` Handled\" on white, the word inside a dark pill scrolls like a slot reel while the pill's width smoothly stretches to fit.",
  "description": "In \"Your `chip` Handled\" on white, the word inside a dark pill scrolls like a slot reel while the pill's width smoothly stretches to fit.",
  "category": "typography",
  "tags": [
    "shotcraft",
    "remotion",
    "typography"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.5,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/pill-chip-slot-cycle-handled/poster-ace1bbc1fb47.jpg",
    "video": "https://gimgs.net/motion/media/pill-chip-slot-cycle-handled/video-f47e273608da.mp4"
  },
  "variants": [
    {
      "key": "pill-chip-slot-cycle-handled",
      "label": "pill-chip-slot-cycle-handled",
      "description": "On a white-background sentence \"Your `chip` Handled,\" the word inside a dark pill cycles vertically like a slot reel, the pill's width smoothly interpolates to a pre-measured text width, the surrounding text is naturally pushed apart or pulled in, and 13%-opacity gray ghost items peek out above and below the pill.",
      "use": "",
      "video": "https://gimgs.net/motion/media/pill-chip-slot-cycle-handled/video-f47e273608da.mp4",
      "poster": "https://gimgs.net/motion/media/pill-chip-slot-cycle-handled/poster-ace1bbc1fb47.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/pill-chip-slot-cycle-handled.html",
  "json_url": "https://gimgs.net/motion/pill-chip-slot-cycle-handled.json",
  "template_url": "https://gimgs.net/motion/pill-chip-slot-cycle-handled/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/pill-chip-slot-cycle-handled.md",
  "updated_at": "2026-09-24T07:40:14.245Z",
  "prompt": "## When to use\n\nCopy patterns like \"we've got ___ handled\"; a one-line close for a SaaS feature list; a hero-visual beat for light-background brand films\n\n## Intention\n\nWord-slot cycling comes in two schools: keep the sentence out of the way of the change (a fixed-width reel), or let the sentence **accept** the change (the pill stretches and the surrounding text gets pushed apart). This card is the latter — the cost is you have to get the width math right, and the payoff is that the sentence reads as alive: \"Your\" and \"Handled\" get pushed along, and the viewer feels the weight of the word inside the pill. The gray ghost items peeking out above and below the pill are a second cue: they signal \"this is a scrollable list,\" priming the viewer to expect the next word.\n\n## Duration & energy\n\n- Duration: About 5.0s (150f@30fps: hold → 3 swaps at 0.25/0.47/0.69 → closing hold)\n- Energy: Medium (a steady three-beat rhythm, width stretching is the only continuous motion)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n词槽轮换有两派：让句子躲开变化（宽度固定的滚轮），或者让句子**接受**\n变化（胶囊撑宽、两侧文字被挤开）。这卡是后者，代价是必须把宽度算准，\n回报是句子读起来活的——\"Your\" 和 \"Handled\" 被推着走，观众感到胶囊里\n的词有分量。上下露出的灰色幽灵项是第二个记号：它明示\"这是个可以滚的\n列表\"，让观众预期还有下一个词。\n\n## 动效核心\n- 宽度预量：setup 期用隐藏 `<span>` 逐词量 `offsetWidth`，各加 74px 内边距\n  存成 `widths[]`（带 `o.w.length·13` 兜底，防 setup 时未挂载量不到）\n- 位置量 `pos` 由 3 段窗累加：切换点 `[0.25, 0.47, 0.69]`，每窗 0.12\n  （≈18f），缓动 `E.inOutCubic`——**两头都缓**，对应\"滚轮起步与停稳\"\n- 内列 `colIn` 竖移 `-pos·48`px（行高 48 = 胶囊高）；胶囊宽度取\n  `lerp(frac, widths[ci], widths[ci+1])`，**帧内插值**所以伸缩是连续的，\n  两侧 `flex:none` 的文字被 flex 布局自然挤开\n- 幽灵项在胶囊外（chipWrap 内）上 -38px / 下 58px，取 `near = round(pos)`\n  的前后邻居文本，随滚动微移 `(pos-near)·48·0.5`（**半速视差**，比胶囊内\n  的词慢一半，才像\"外圈\"）\n- 幽灵项透明度 `0.13 · (0.4 + settle·0.6)`，`settle = 1 - min(1, |pos-near|·3)`\n  ——滚动中途更淡、落定时最清楚\n- 每行内是 emoji + 词的两段结构（emoji 18px、词 700 22px 白字），\n  胶囊 `#1a1c24` 底 + `0 8px 24px rgba(20,22,40,.22)` 投影 + `overflow:hidden`\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 词表 | 4 词 + emoji（Sales/Workflow/Admin/Reports），3 次切换 | 词长差决定挤开幅度的观感：demo 最短 5 字母最长 8 字母；差 >2 倍胶囊跳变太猛，句子重心左右甩 |\n| 切换点 | `[0.25, 0.47, 0.69]`，窗 0.12（≈18f） | 段间约 0.10（≈15f）静置读词；起手 0.25 前的静置让观众先认出句式 |\n| 缓动 | `E.inOutCubic` | 两头缓才像滚轮起停；outCubic（只后缓）读作被甩上来，胶囊宽度也会跟着突变 |\n| 胶囊 | 高 48px、圆角 99、`#1a1c24`、宽 = 文本宽 + 74 | +74 是左右内边距 + emoji 位；<50 词贴边，>100 胶囊胖成按钮 |\n| 幽灵项 | 上 -38 / 下 58px，基础透明度 0.13 | 0.13 是\"看得见但不抢\"的甜点；>0.25 读作三行并列文字，不是幽灵；删了列表暗示就没了 |\n| 幽灵视差 | `(pos-near)·48·0.5`（半速） | 半速是\"外圈更远\"的立体感来源；同速则三行像一整块滑动，幽灵失去意义 |\n| 句子字号 | 两侧 30px/800、胶囊内 22px/700 | 胶囊内比句子小是正确层级（徽章依附句子）；等大或更大就读作两个标题打架 |\n| 配色 | 底 `#fbfbfd`、句子 `#15171d`、胶囊白字 | 浅底 + 单一深色块是本卡的身份证；深底版会让胶囊消失，需整套重配 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- **宽度靠 setup 期 `offsetWidth` 实测**：字体没加载完时量到的是回退字体\n  的宽度，胶囊会偏窄或偏宽——移植时须确保 webfont 就绪后再量，或改用\n  离屏 canvas `measureText`；`length·13` 那条兜底只保证不崩，不保证准\n- 词长差要控制在 2 倍内：宽度自适应是本卡的卖点也是风险，\n  `Sales`→`Workflow` 这一跳已经是可接受上限，再大句子两侧甩得晃眼\n- 两侧文字必须 `flex:none`：不然 flex 会去压缩它们而不是移动它们，\n  \"挤开\"变成\"字被压扁\"\n- 幽灵项文本取 `round(pos)` 的邻居，所以在切换的正中间（pos=x.5）会\n  **跳一次文本**——被 `settle` 压到最淡时发生，肉眼基本不可察；但若把\n  透明度调高到 0.25+，这一跳就会露出来\n- emoji 在不同平台字形宽度不一（尤其 `⚙️` 带变体选择符），会让实测宽度\n  漂移几像素；实战建议换成项目自己的单色图标，别依赖系统 emoji\n- 与 vertical-word-roll-blur-cycle 分工：那卡宽度固定、裸词、带滚筒 blur\n  （词是句子成分），本卡宽度自适应、深色胶囊、带幽灵项（词是徽章）——\n  同片只留一个；声音上三次落定是三个节奏点，配轻质开关/胶囊贴合音，\n  连发交替双样本，配法纪律见 sound-design.md\n\n## 参考实现\ndemos/typography/pill-chip-slot-cycle-handled/\n（PillChipSlotCycleHandled.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 165,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx =====\n// pill-chip-slot-cycle-handled — Chip Slot Cycle 胶囊滚轮挤开（motion-lab 定稿转原生 Remotion）\n// 白底句式 \"Your [chip] Handled\"：深色胶囊内词垂直滚轮轮换（Sales→Workflow→Admin→Reports），\n// 上下露出灰色幽灵项，胶囊宽度随词长平滑变化，两侧文字被自然挤开收拢。\n// 设计坐标 480×270（DesignStage 等比放大），参数表数值以此坐标系标定。\nimport React, { useLayoutEffect, useRef, useState } from 'react';\nimport { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';\n\nexport const PILL_CHIP_SLOT_CYCLE_HANDLED_DURATION = 150; // 5000ms @30fps\n\nconst WORDS = [\n  { w: 'Sales', e: '⚡' },\n  { w: 'Workflow', e: '📈' },\n  { w: 'Admin', e: '⚙️' },\n  { w: 'Reports', e: '📄' },\n];\nconst FONT = '700 22px -apple-system,system-ui,sans-serif';\n// 兜底词宽（原 effect.js 未挂载时的估算：字符数×13，再加胶囊内留白 74）；\n// 首帧 useLayoutEffect 用 offsetWidth 实测替换，与原渲染逐像素一致\nconst FALLBACK_WIDTHS = WORDS.map((o) => o.w.length * 13 + 74);\n\n// 两侧静态文字（\"Your\" / \"Handled\"）\nconst SIDE: React.CSSProperties = {\n  color: '#15171d',\n  fontWeight: 800,\n  fontSize: 30,\n  letterSpacing: -0.5,\n  flex: 'none',\n};\n\nexport const PillChipSlotCycleHandled: React.FC = () => {\n  const t = useT();\n\n  // 量宽：与原 effect.js 相同，用隐藏 span 实测每个词的 offsetWidth（+74 胶囊留白），\n  // 只在首次布局量一次；量到前用兜底估算，避免首帧闪动\n  const measRef = useRef<HTMLDivElement>(null);\n  const [widths, setWidths] = useState<number[]>(FALLBACK_WIDTHS);\n  useLayoutEffect(() => {\n    const spans = measRef.current?.children;\n    if (!spans) return;\n    setWidths(\n      WORDS.map((o, i) => ((spans[i] as HTMLElement).offsetWidth || o.w.length * 13) + 74),\n    );\n  }, []);\n\n  // 三次切换：0.25 / 0.47 / 0.69，每次用 0.12 的 inOutCubic 推进一格\n  let pos = 0;\n  for (const s0 of [0.25, 0.47, 0.69]) pos += seg(t, s0, s0 + 0.12, E.inOutCubic);\n  const ci = Math.min(WORDS.length - 1, Math.floor(pos));\n  const frac = pos - ci;\n  // 胶囊宽度随当前词→下一词插值，平滑挤开两侧文字\n  const chipW = lerp(frac, widths[ci], widths[Math.min(ci + 1, WORDS.length - 1)]);\n\n  // 幽灵项：当前词的前/后邻居，随滚动微移，落定时回到基准透明度\n  const near = Math.round(pos);\n  const roll = (pos - near) * 48 * 0.5;\n  const settle = 1 - Math.min(1, Math.abs(pos - near) * 3);\n  const ghost = (top: number): React.CSSProperties => ({\n    position: 'absolute',\n    left: '50%',\n    top,\n    font: FONT,\n    color: '#15171d',\n    whiteSpace: 'nowrap',\n    transform: `translateX(-50%) translateY(${-roll}px)`,\n    opacity: 0.13 * (0.4 + settle * 0.6),\n  });\n\n  return (\n    <DesignStage bg=\"#fbfbfd\">\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          fontFamily: '-apple-system,system-ui,sans-serif',\n        }}\n      >\n        {/* 隐藏量宽 span（与原 setup 的 meas 等价，不参与布局显示） */}\n        <div ref={measRef} style={{ position: 'absolute', visibility: 'hidden' }}>\n          {WORDS.map((o) => (\n            <span key={o.w} style={{ whiteSpace: 'nowrap', font: FONT }}>\n              {o.w}\n            </span>\n          ))}\n        </div>\n        <div style={SIDE}>Your</div>\n        <div style={{ position: 'relative', flex: 'none' }}>\n          {/* 深色胶囊：overflow hidden 里放 4 行词的滚轮列 */}\n          <div\n            style={{\n              position: 'relative',\n              height: 48,\n              width: chipW,\n              margin: '0 14px',\n              flex: 'none',\n              borderRadius: 99,\n              background: '#1a1c24',\n              boxShadow: '0 8px 24px rgba(20,22,40,.22)',\n              overflow: 'hidden',\n            }}\n          >\n            <div\n              style={{\n                position: 'absolute',\n                left: 0,\n                right: 0,\n                top: 0,\n                transform: `translateY(${-pos * 48}px)`,\n              }}\n            >\n              {WORDS.map(({ w, e }) => (\n                <div\n                  key={w}\n                  style={{\n                    height: 48,\n                    display: 'flex',\n                    alignItems: 'center',\n                    gap: 9,\n                    paddingLeft: 18,\n                    whiteSpace: 'nowrap',\n                  }}\n                >\n                  <span style={{ fontSize: 18 }}>{e}</span>\n                  <span style={{ font: FONT, color: '#fff' }}>{w}</span>\n                </div>\n              ))}\n            </div>\n          </div>\n          {/* 胶囊外上下的灰色幽灵项 */}\n          <div style={ghost(-38)}>{near > 0 ? WORDS[near - 1].w : ''}</div>\n          <div style={ghost(58)}>{near < WORDS.length - 1 ? WORDS[near + 1].w : ''}</div>\n        </div>\n        <div style={SIDE}>Handled</div>\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}