{
  "name": "ai-composer-utils",
  "type": "registry:lib",
  "description": "Shared typewriter, caret, morph, and intro helpers for AI composer scenes",
  "dependencies": [
    "remotion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/bases/default/lib/ai-composer-utils.tsx",
      "type": "registry:lib",
      "content": "import type { CSSProperties } from \"react\";\nimport { interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\n\n/** Frame the prompt starts typing in the chat-style composers. */\nexport const AI_TYPING_START = 42;\n/** Frame the prompt starts typing in the terminal-style composers. */\nexport const AI_TYPING_START_TUI = 48;\n/** Characters per second for the chat-style composers. */\nexport const AI_TYPING_CPS = 22;\n\nexport function stageScale(\n  width: number,\n  height: number,\n  refW = 1280,\n  refH = 720,\n): number {\n  return Math.min(width / refW, height / refH);\n}\n\nexport interface TypewriterOptions {\n  cps?: number;\n  speed?: number;\n  startFrame?: number;\n}\n\nexport interface TypewriterState {\n  text: string;\n  count: number;\n  done: boolean;\n  typing: boolean;\n}\n\n/**\n * Character-by-character reveal. `typing` is false both before the first\n * character and after the last one, so a caret bound to `!typing` blinks while\n * idle and holds solid while text is being revealed.\n */\nexport function useTypewriter(\n  full: string,\n  options: TypewriterOptions = {},\n): TypewriterState {\n  const {\n    cps = AI_TYPING_CPS,\n    speed = 1,\n    startFrame = AI_TYPING_START,\n  } = options;\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n  const local = frame * speed - startFrame;\n  const over = (full.length / cps) * fps;\n  const count =\n    local <= 0\n      ? 0\n      : over <= 0\n        ? full.length\n        : Math.max(0, Math.min(full.length, Math.floor((local / over) * full.length)));\n\n  return {\n    text: full.slice(0, count),\n    count,\n    done: count >= full.length,\n    typing: count > 0 && count < full.length,\n  };\n}\n\n/** Spring that drives the mic/waveform → send button morph. */\nexport function morphProgressAt(\n  frame: number,\n  opts: { startFrame?: number; fps: number; speed?: number },\n): number {\n  const { startFrame = AI_TYPING_START, fps, speed = 1 } = opts;\n  const value = spring({\n    fps,\n    frame: frame * speed - startFrame,\n    config: { damping: 14, stiffness: 200, mass: 0.6 },\n  });\n  return Math.max(0, Math.min(value, 1));\n}\n\nexport function introBounceIn(\n  frame: number,\n  fps: number,\n): { translateY: number; scale: number } {\n  const s = spring({\n    fps,\n    frame,\n    config: { damping: 14, stiffness: 110, mass: 0.7 },\n  });\n  return {\n    translateY: interpolate(s, [0, 1], [28, 0]),\n    scale: interpolate(s, [0, 1], [0.97, 1]),\n  };\n}\n\nexport function fadeUpAt(\n  frame: number,\n  range: [number, number],\n): { opacity: number; translateY: number } {\n  const opts = {\n    extrapolateLeft: \"clamp\" as const,\n    extrapolateRight: \"clamp\" as const,\n  };\n  return {\n    opacity: interpolate(frame, range, [0, 1], opts),\n    translateY: interpolate(frame, range, [12, 0], opts),\n  };\n}\n\nexport function caretBlinkOpacity(\n  frame: number,\n  opts: { fps: number; blinkPerSecond: number; speed: number },\n): number {\n  const cycles = opts.blinkPerSecond <= 0 ? 1 : opts.blinkPerSecond;\n  const halfPeriod = opts.fps / cycles / 2;\n  if (halfPeriod <= 0) return 1;\n  return Math.floor((frame * opts.speed) / halfPeriod) % 2 === 0 ? 1 : 0;\n}\n\nexport interface CaretProps {\n  color?: string;\n  width?: number;\n  height?: number;\n  radius?: number;\n  opacity?: number;\n  blink?: boolean;\n  blinkPerSecond?: number;\n  speed?: number;\n  marginLeft?: number;\n  style?: CSSProperties;\n}\n\nexport function Caret({\n  color = \"currentColor\",\n  width = 2,\n  height = 18,\n  radius = 1,\n  opacity,\n  blink = false,\n  blinkPerSecond = 1,\n  speed = 1,\n  marginLeft = 0,\n  style,\n}: CaretProps) {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  const resolvedOpacity =\n    opacity !== undefined\n      ? opacity\n      : blink\n        ? caretBlinkOpacity(frame, { fps, blinkPerSecond, speed })\n        : 1;\n\n  return (\n    <span\n      style={{\n        display: \"inline-block\",\n        flexShrink: 0,\n        width,\n        height,\n        borderRadius: radius,\n        background: color,\n        opacity: resolvedOpacity,\n        marginLeft,\n        ...style,\n      }}\n    />\n  );\n}\n"
    }
  ]
}