{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caption-label-maker",
  "title": "Caption Label Maker",
  "description": "Each phrase is punched letter by letter into a strip of embossing tape at the pace it is spoken, the tape feeding out as the words arrive.",
  "dependencies": ["remotion", "@remotion/captions"],
  "registryDependencies": ["@remocn/caption-core"],
  "files": [
    {
      "path": "registry/remocn/caption-label-maker/index.tsx",
      "content": "\"use client\";\n\nimport { type CSSProperties, Fragment } from \"react\";\nimport { Easing, interpolate } from \"remotion\";\nimport {\n  type CaptionBaseProps,\n  captionWordText,\n  DEFAULT_HOLD_MS,\n  getExitWindowMs,\n  groupCaptionWords,\n  useCaptions,\n} from \"@/lib/remocn/caption-core\";\n\nexport interface CaptionLabelMakerProps extends CaptionBaseProps {\n  tapeColor?: string;\n}\n\nconst ENTER_MS = 160;\nconst EXIT_MS = 220;\nconst RISE_EM = 0.14;\nconst PRESS_MS = 50;\nconst PRESS_SCALE = 0.92;\nconst TILT_DEG = -1.5;\nconst PAD_Y = 0.16;\nconst LEAD_EM = 0.42;\nconst RADIUS = 0.16;\nconst EMBOSS = \"0 1px 0 rgba(0, 0, 0, 0.55)\";\nconst RELIEF =\n  \"inset 0 1px 0 rgba(255, 255, 255, 0.14), inset 0 -1px 0 rgba(0, 0, 0, 0.5)\";\n\nconst enterEasing = Easing.bezier(0.2, 0.8, 0.2, 1);\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nfunction revealedCount(\n  count: number,\n  fromMs: number,\n  toMs: number,\n  timeMs: number,\n): number {\n  if (timeMs < fromMs) return 0;\n  const duration = Math.max(toMs - fromMs, 1);\n  return Math.min(\n    count,\n    Math.floor(((timeMs - fromMs) / duration) * count) + 1,\n  );\n}\n\nexport function CaptionLabelMaker({\n  captions,\n  combineTokensWithinMilliseconds = 1000,\n  holdMs = DEFAULT_HOLD_MS,\n  fontSize = 64,\n  fontWeight = 800,\n  color = \"#ffffff\",\n  tapeColor = \"#18181b\",\n  className,\n}: CaptionLabelMakerProps) {\n  const state = useCaptions({\n    captions,\n    combineTokensWithinMilliseconds,\n    holdMs,\n  });\n  if (!state) return null;\n\n  const enter = interpolate(\n    state.previous ? ENTER_MS : state.sinceStartMs,\n    [0, ENTER_MS],\n    [0, 1],\n    { ...clamp, easing: enterEasing },\n  );\n  const exit = state.endsInSilence\n    ? interpolate(\n        state.untilEndMs,\n        [0, getExitWindowMs(state, EXIT_MS)],\n        [0, 1],\n        clamp,\n      )\n    : 1;\n\n  const segment: CSSProperties = {\n    display: \"inline-block\",\n    verticalAlign: \"top\",\n    background: tapeColor,\n    paddingBlock: `${PAD_Y}em`,\n    paddingRight: 1,\n    marginRight: -1,\n    boxShadow: RELIEF,\n  };\n\n  const words = groupCaptionWords(state.tokens).map((word) => {\n    const chars = Array.from(captionWordText(word));\n    const fromMs = word[0].token.fromMs;\n    const toMs = word[word.length - 1].token.toMs;\n    return {\n      key: `${state.pageIndex}-${word[0].index}`,\n      chars,\n      fromMs,\n      toMs,\n      revealed: revealedCount(chars.length, fromMs, toMs, state.timeMs),\n    };\n  });\n  const last = words[words.length - 1];\n  const finished = last !== undefined && last.revealed === last.chars.length;\n\n  return (\n    <div\n      className={className}\n      style={{\n        fontSize,\n        fontWeight,\n        color,\n        lineHeight: 1.1,\n        textAlign: \"center\",\n        opacity: Math.min(enter, exit),\n        transform: `translateY(${(1 - enter) * RISE_EM}em)`,\n      }}\n    >\n      <span\n        style={{\n          display: \"inline-block\",\n          whiteSpace: \"nowrap\",\n          transform: `rotate(${TILT_DEG}deg)`,\n          filter: \"drop-shadow(0 2px 4px rgba(0, 0, 0, 0.35))\",\n        }}\n      >\n        <span\n          style={{\n            ...segment,\n            width: `${LEAD_EM}em`,\n            borderRadius: `${RADIUS}em 0 0 ${RADIUS}em`,\n          }}\n        >\n          {\"\\u00a0\"}\n        </span>\n        {words.map((word, w) => {\n          const duration = Math.max(word.toMs - word.fromMs, 1);\n          return (\n            <Fragment key={word.key}>\n              {w > 0 && (\n                <span\n                  style={{\n                    ...segment,\n                    visibility: word.revealed > 0 ? \"visible\" : \"hidden\",\n                  }}\n                >\n                  {\"\\u00a0\"}\n                </span>\n              )}\n              {word.chars.map((char, c) => {\n                const pressedAt =\n                  word.fromMs + (c / word.chars.length) * duration;\n                const press = interpolate(\n                  state.timeMs - pressedAt,\n                  [0, PRESS_MS],\n                  [PRESS_SCALE, 1],\n                  clamp,\n                );\n                return (\n                  <span\n                    key={c}\n                    style={{\n                      ...segment,\n                      visibility: c < word.revealed ? \"visible\" : \"hidden\",\n                    }}\n                  >\n                    <span\n                      style={{\n                        display: \"inline-block\",\n                        transform: `scale(${press})`,\n                        textShadow: EMBOSS,\n                      }}\n                    >\n                      {char}\n                    </span>\n                  </span>\n                );\n              })}\n            </Fragment>\n          );\n        })}\n        <span\n          style={{\n            ...segment,\n            width: `${LEAD_EM}em`,\n            borderRadius: `0 ${RADIUS}em ${RADIUS}em 0`,\n            visibility: finished ? \"visible\" : \"hidden\",\n          }}\n        >\n          {\"\\u00a0\"}\n        </span>\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/caption-label-maker.tsx"
    }
  ],
  "type": "registry:component"
}
