{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "batchwork",
  "title": "Batchwork — Unified Batch API",
  "description": "remocn demo composition \"Batchwork — Unified Batch API\" — installs the full Remotion composition. Generated with AI from the prompt in demos/batchwork/prompt.md.",
  "dependencies": [
    "@remotion/google-fonts",
    "@remotion/transitions",
    "culori",
    "remotion"
  ],
  "registryDependencies": [
    "https://remocn.dev/r/backdrop.json",
    "https://remocn.dev/r/blur-in.json",
    "https://remocn.dev/r/marker-highlight.json",
    "https://remocn.dev/r/remocn-ui.json"
  ],
  "files": [
    {
      "path": "src/components/remocn/glass-code-block.tsx",
      "content": "\"use client\";\n\nimport {\n  Sequence,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport interface GlassCodeBlockProps {\n  code?: string;\n  title?: string;\n  width?: number;\n  height?: number;\n  fontSize?: number;\n  glassColor?: string;\n  staggerFrames?: number;\n  showTrafficLights?: boolean;\n  /** The soft cyan/purple gradient glow behind the glass. On by default. */\n  aura?: boolean;\n  /**\n   * Reveal the first line as a left-to-right typewriter (with a block cursor)\n   * instead of the default fade-up; the remaining lines then stagger in only\n   * after the first line finishes typing. Off by default.\n   */\n  typeFirstLine?: boolean;\n  /** Typing speed (characters/second) when `typeFirstLine` is on. */\n  firstLineCps?: number;\n  speed?: number;\n  className?: string;\n}\n\n/** Frames the first-line typewriter takes — exported so a caller can sync a\n *  camera move to the moment typing completes. */\nexport function firstLineTypeFrames(\n  firstLine: string,\n  firstLineCps: number,\n  fps: number,\n): number {\n  return Math.ceil((firstLine.length / firstLineCps) * fps);\n}\n\nconst FONT_MONO =\n  \"var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace\";\n\nconst DEFAULT_CODE = `import { motion } from \"remotion\";\n\n// Generate a hero scene\nexport function Hero() {\n  const frame = useCurrentFrame();\n  const opacity = frame / 30;\n  return <h1 style={{ opacity }}>Hello</h1>;\n}`;\n\n// Minimal regex tokenizer. NOT a real syntax highlighter — just enough to\n// give the eye color anchors. Order matters: comments → strings → keywords.\nconst KEYWORDS = new Set([\n  \"import\",\n  \"from\",\n  \"export\",\n  \"function\",\n  \"const\",\n  \"let\",\n  \"var\",\n  \"return\",\n  \"if\",\n  \"else\",\n  \"for\",\n  \"while\",\n  \"new\",\n  \"class\",\n  \"extends\",\n  \"default\",\n  \"true\",\n  \"false\",\n  \"null\",\n  \"undefined\",\n]);\n\ntype Token = {\n  text: string;\n  kind: \"code\" | \"comment\" | \"string\" | \"keyword\" | \"number\";\n};\n\nfunction tokenizeLine(line: string): Token[] {\n  // Whole-line comment.\n  const trimmed = line.trimStart();\n  if (trimmed.startsWith(\"//\")) {\n    return [{ text: line, kind: \"comment\" }];\n  }\n\n  const tokens: Token[] = [];\n  // Split keeping delimiters: words, strings, numbers, everything else.\n  const re = /(\"[^\"]*\"|'[^']*'|`[^`]*`|\\b\\d+\\b|\\b[A-Za-z_$][\\w$]*\\b|[^\\w\"']+)/g;\n  let match: RegExpExecArray | null;\n  while ((match = re.exec(line)) !== null) {\n    const t = match[0];\n    const first = t[0];\n    if (first === '\"' || first === \"'\" || first === \"`\") {\n      tokens.push({ text: t, kind: \"string\" });\n    } else if (/^\\d+$/.test(t)) {\n      tokens.push({ text: t, kind: \"number\" });\n    } else if (/^[A-Za-z_$][\\w$]*$/.test(t) && KEYWORDS.has(t)) {\n      tokens.push({ text: t, kind: \"keyword\" });\n    } else {\n      tokens.push({ text: t, kind: \"code\" });\n    }\n  }\n  return tokens;\n}\n\nconst TOKEN_COLORS: Record<Token[\"kind\"], string> = {\n  code: \"#e4e4e7\",\n  comment: \"#52525b\",\n  string: \"#86efac\",\n  keyword: \"#c4b5fd\",\n  number: \"#fcd34d\",\n};\n\nexport function GlassCodeBlock({\n  code = DEFAULT_CODE,\n  title = \"hero.tsx\",\n  width = 760,\n  height = 460,\n  fontSize = 16,\n  glassColor = \"rgba(10, 10, 10, 0.6)\",\n  staggerFrames = 4,\n  showTrafficLights = true,\n  aura = true,\n  typeFirstLine = false,\n  firstLineCps = 30,\n  speed = 1,\n  className,\n}: GlassCodeBlockProps) {\n  const { fps } = useVideoConfig();\n  const lines = code.split(\"\\n\");\n  // When the first line types, every later line waits until typing is done.\n  const typeDur = typeFirstLine\n    ? firstLineTypeFrames(lines[0] ?? \"\", firstLineCps, fps)\n    : 0;\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      {/* Animated background hint behind the glass so the blur has something\n          to chew on. Pure CSS — no extra deps. */}\n      {aura && <BackdropAura />}\n\n      {/* 1px gradient ring acting as a microborder */}\n      <div\n        style={{\n          position: \"relative\",\n          padding: 1,\n          borderRadius: 16,\n          background:\n            \"linear-gradient(180deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 100%)\",\n          width,\n          height,\n          boxShadow: \"0 50px 120px rgba(0,0,0,0.55)\",\n        }}\n      >\n        <div\n          style={{\n            width: \"100%\",\n            height: \"100%\",\n            borderRadius: 15,\n            background: glassColor,\n            backdropFilter: \"blur(16px)\",\n            WebkitBackdropFilter: \"blur(16px)\",\n            boxShadow: \"inset 0 1px 0 rgba(255,255,255,0.06)\",\n            display: \"flex\",\n            flexDirection: \"column\",\n            overflow: \"hidden\",\n            fontFamily: FONT_MONO,\n          }}\n        >\n          {/* Chrome */}\n          <div\n            style={{\n              height: 40,\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: 8,\n              padding: \"0 16px\",\n              borderBottom: \"1px solid rgba(255,255,255,0.06)\",\n            }}\n          >\n            {showTrafficLights && (\n              <>\n                <Light color=\"#ff5f57\" />\n                <Light color=\"#febc2e\" />\n                <Light color=\"#28c840\" />\n              </>\n            )}\n            <div\n              style={{\n                flex: 1,\n                textAlign: \"center\",\n                color: \"#a1a1aa\",\n                fontSize: 12,\n                letterSpacing: \"0.02em\",\n              }}\n            >\n              {title}\n            </div>\n          </div>\n\n          {/* Code body */}\n          <div\n            style={{\n              flex: 1,\n              padding: \"20px 24px\",\n              display: \"flex\",\n              flexDirection: \"column\",\n              gap: 4,\n              fontSize,\n              lineHeight: 1.55,\n            }}\n          >\n            {lines.map((line, i) => {\n              if (typeFirstLine && i === 0) {\n                return (\n                  <Sequence key={i} from={0} layout=\"none\">\n                    <TypedCodeLine\n                      line={line}\n                      index={i}\n                      fontSize={fontSize}\n                      cps={firstLineCps}\n                      fps={fps}\n                      speed={speed}\n                    />\n                  </Sequence>\n                );\n              }\n              const baseFrom = typeFirstLine\n                ? typeDur + (i - 1) * staggerFrames\n                : i * staggerFrames;\n              return (\n                <Sequence\n                  key={i}\n                  from={Math.round(baseFrom / speed)}\n                  layout=\"none\"\n                >\n                  <CodeLine line={line} index={i} fontSize={fontSize} />\n                </Sequence>\n              );\n            })}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction Light({ color }: { color: string }) {\n  return (\n    <div\n      style={{\n        width: 12,\n        height: 12,\n        borderRadius: \"50%\",\n        background: color,\n        opacity: 0.6,\n      }}\n    />\n  );\n}\n\nfunction CodeLine({\n  line,\n  index,\n  fontSize,\n}: {\n  line: string;\n  index: number;\n  fontSize: number;\n}) {\n  const frame = useCurrentFrame();\n  const opacity = interpolate(frame, [0, 8], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const ty = interpolate(frame, [0, 8], [4, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  const tokens = tokenizeLine(line);\n  // Render an empty line as a half-height spacer so blank lines still take\n  // visual space without collapsing the gap.\n  if (tokens.length === 0) {\n    return <div style={{ height: fontSize * 0.8, opacity }} />;\n  }\n  return (\n    <div\n      style={{\n        opacity,\n        transform: `translateY(${ty}px)`,\n        whiteSpace: \"pre\",\n        display: \"flex\",\n        gap: 0,\n      }}\n    >\n      <span style={{ width: 28, color: \"#3f3f46\", userSelect: \"none\" }}>\n        {String(index + 1).padStart(2, \" \")}\n      </span>\n      <span>\n        {tokens.map((t, i) => (\n          <span key={i} style={{ color: TOKEN_COLORS[t.kind] }}>\n            {t.text}\n          </span>\n        ))}\n      </span>\n    </div>\n  );\n}\n\nfunction TypedCodeLine({\n  line,\n  index,\n  fontSize,\n  cps,\n  fps,\n  speed,\n}: {\n  line: string;\n  index: number;\n  fontSize: number;\n  cps: number;\n  fps: number;\n  speed: number;\n}) {\n  const frame = useCurrentFrame() * speed;\n  const len = line.length;\n  const revealed = Math.floor(\n    interpolate(frame, [0, (len / cps) * fps], [0, len], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n    }),\n  );\n  const visible = line.substring(0, revealed);\n  const done = revealed >= len;\n  // 2 Hz blink at any framerate.\n  const cursorOn = Math.floor((frame / fps) * 2) % 2 === 0;\n  const tokens = tokenizeLine(visible);\n\n  return (\n    <div\n      style={{\n        whiteSpace: \"pre\",\n        display: \"flex\",\n        gap: 0,\n        alignItems: \"center\",\n      }}\n    >\n      <span style={{ width: 28, color: \"#3f3f46\", userSelect: \"none\" }}>\n        {String(index + 1).padStart(2, \" \")}\n      </span>\n      <span>\n        {tokens.map((t, i) => (\n          <span key={i} style={{ color: TOKEN_COLORS[t.kind] }}>\n            {t.text}\n          </span>\n        ))}\n        {!done && cursorOn && (\n          <span\n            style={{\n              display: \"inline-block\",\n              width: fontSize * 0.55,\n              height: fontSize,\n              background: \"#e4e4e7\",\n              marginLeft: 1,\n              transform: \"translateY(2px)\",\n            }}\n          />\n        )}\n      </span>\n    </div>\n  );\n}\n\nfunction BackdropAura() {\n  // Slow, low-amplitude blob behind the glass so the backdrop blur has\n  // perceptible content to refract.\n  const frame = useCurrentFrame();\n  const t = frame / 60;\n  const x = 50 + Math.sin(t) * 20;\n  const y = 50 + Math.cos(t * 0.7) * 15;\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        pointerEvents: \"none\",\n        background: `radial-gradient(circle at ${x}% ${y}%, rgba(56,189,248,0.22), transparent 50%), radial-gradient(circle at ${100 - x}% ${100 - y}%, rgba(168,85,247,0.18), transparent 55%)`,\n      }}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/glass-code-block.tsx"
    },
    {
      "path": "src/demos/batchwork/index.tsx",
      "content": "import React, { type CSSProperties, type ReactNode } from \"react\";\nimport { AbsoluteFill, Easing, interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { demoAsset } from \"@/lib/demo-assets\";\nimport {\n  TransitionSeries,\n  linearTiming,\n  type TransitionPresentation,\n  type TransitionPresentationComponentProps,\n} from \"@remotion/transitions\";\nimport { loadFont as loadSans } from \"@remotion/google-fonts/Manrope\";\nimport { loadFont as loadMono } from \"@remotion/google-fonts/GeistMono\";\n\nimport { RemocnUIProvider } from \"@/lib/remocn-ui\";\nimport { Backdrop } from \"@/components/remocn/backdrop\";\nimport { GlassCodeBlock } from \"@/components/remocn/glass-code-block\";\nimport { MarkerHighlight } from \"@/components/remocn/marker-highlight\";\nimport { useBlurInTransition } from \"@/components/remocn/use-blur-in-transition\";\nimport { BlurIn, type BlurInDirection } from \"@/components/remocn/blur-in\";\n\n// Bind both fonts to the CSS variables the remocn components read.\nconst { fontFamily: SANS_FAMILY } = loadSans(\"normal\", {\n  subsets: [\"latin\"],\n  weights: [\"400\", \"600\", \"700\", \"800\"],\n});\nconst { fontFamily: MONO_FAMILY } = loadMono(\"normal\", {\n  subsets: [\"latin\"],\n  weights: [\"400\", \"700\"],\n});\n\nconst SANS = \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\";\nconst MONO = \"var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace\";\n\nconst GREEN = \"#4ade80\";\nconst YELLOW = \"#facc15\";\n\n// ---------------------------------------------------------------------------\n// Scene timings (frames @ 30fps). Transitions overlap consecutive scenes.\n// ---------------------------------------------------------------------------\nconst S1 = 76; // hook — holds long enough for the headline to land\nconst S2 = 58; // subtitle + install\nconst S3 = 86; // one API for every batch provider\nconst S4 = 90; // works with any AI SDK model\nconst S5 = 82; // automatic polling & webhooks\nconst S6 = 90; // drop straight into Next.js\nconst S7 = 100; // final logo\n\n// Transition durations — quick enough to feel snappy, long enough to read the\n// motion (slide + blur + scale).\nconst TI = 18; // intro → install (zoom-blur push)\nconst TK = 16; // kinetic slide between content scenes\nconst TO = 20; // into the final logo\n\nexport const BATCHWORK_DURATION =\n  S1 + S2 + S3 + S4 + S5 + S6 + S7 - (TI + 4 * TK + TO);\n\n// ---------------------------------------------------------------------------\n// Reveal — blur-in wrapper driven by useBlurInTransition.\n// ---------------------------------------------------------------------------\nconst Reveal: React.FC<{\n  children: ReactNode;\n  delay?: number;\n  direction?: BlurInDirection;\n  distance?: number;\n  blur?: number;\n  duration?: number;\n  display?: CSSProperties[\"display\"];\n}> = ({\n  children,\n  delay = 0,\n  direction = \"up\",\n  distance = 16,\n  blur = 10,\n  duration = 20,\n  display = \"block\",\n}) => {\n  const style = useBlurInTransition(\n    [{ at: delay, state: \"revealed\", duration }],\n    { direction, distance, blur },\n  );\n\n  return (\n    <BlurIn style={style} display={display}>\n      {children}\n    </BlurIn>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// Mark — inline marker-highlight swipe behind a phrase, tuned for the dark\n// backdrop (white text that turns dark as the marker arrives).\n// ---------------------------------------------------------------------------\nconst Mark: React.FC<{\n  children: string;\n  color?: string;\n  after?: string;\n}> = ({ children, color = GREEN, after = \"\" }) => (\n  <MarkerHighlight\n    highlight={children}\n    after={after}\n    markerColor={color}\n    baseColor=\"#fafafa\"\n    highlightedTextColor=\"#0a0a0a\"\n  />\n);\n\n// ---------------------------------------------------------------------------\n// Provider icons — circular brand monograms.\n// ---------------------------------------------------------------------------\nconst PROVIDERS = [\n  { label: \"OpenAI\", glyph: \"✸\", bg: \"#0a0a0a\", fg: \"#ffffff\" },\n  { label: \"Claude\", glyph: \"✳\", bg: \"#d97757\", fg: \"#ffffff\" },\n  { label: \"Mistral\", glyph: \"▲\", bg: \"linear-gradient(135deg,#ff7000,#ffd21e)\", fg: \"#0a0a0a\" },\n  { label: \"xAI\", glyph: \"𝕏\", bg: \"#111111\", fg: \"#ffffff\" },\n  { label: \"Gemini\", glyph: \"✦\", bg: \"linear-gradient(135deg,#4796e3,#9177c7,#d96570)\", fg: \"#ffffff\" },\n  { label: \"Perplexity\", glyph: \"≈\", bg: \"#20808d\", fg: \"#ffffff\" },\n];\n\nconst ProviderIcons: React.FC<{\n  size?: number;\n  baseDelay?: number;\n  /** How far each icon slides under the previous one, in px. */\n  overlap?: number;\n}> = ({ size = 110, baseDelay = 4, overlap }) => {\n  const ov = overlap ?? Math.round(size * 0.36);\n  return (\n    <div style={{ display: \"flex\", alignItems: \"center\" }}>\n      {PROVIDERS.map((p, i) => (\n        <div\n          key={p.label}\n          style={{\n            marginLeft: i === 0 ? 0 : -ov,\n            // Left icons sit on top so the stack reads as a left-to-right fan.\n            zIndex: PROVIDERS.length - i,\n          }}\n        >\n          <Reveal delay={baseDelay + i * 4} distance={16} blur={8}>\n            <div\n              style={{\n                width: size,\n                height: size,\n                borderRadius: \"50%\",\n                background: p.bg,\n                color: p.fg,\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n                fontSize: size * 0.42,\n                fontWeight: 700,\n                fontFamily: SANS,\n                // Dark ring separates the overlapping circles, plus drop shadow.\n                boxShadow:\n                  \"0 0 0 5px rgba(10,10,10,0.9), 0 16px 36px rgba(0,0,0,0.5)\",\n                border: \"1px solid rgba(255,255,255,0.18)\",\n              }}\n            >\n              {p.glyph}\n            </div>\n          </Reveal>\n        </div>\n      ))}\n    </div>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// Install pill — $ npm install batchwork.\n// ---------------------------------------------------------------------------\nconst InstallPill: React.FC<{ fontSize?: number }> = () => (\n  <div\n    style={{\n      display: \"inline-flex\",\n      alignItems: \"center\",\n      gap: 10,\n      padding: \"12px 24px\",\n      borderRadius: 999,\n      background: \"rgba(10,10,10,0.55)\",\n      border: \"1px solid rgba(255,255,255,0.14)\",\n      boxShadow: \"0 16px 40px rgba(0,0,0,0.4)\",\n      fontFamily: MONO,\n      fontSize: 18,\n      letterSpacing: \"-0.01em\",\n    }}\n  >\n    <span style={{ color: GREEN }}>$</span>\n    <span style={{ color: \"#d4d4d8\" }}>npm install</span>\n    <span style={{ color: \"#ffffff\", fontWeight: 700 }}>batchwork</span>\n  </div>\n);\n\n// ---------------------------------------------------------------------------\n// Shared text styles.\n// ---------------------------------------------------------------------------\nconst headingStyle: CSSProperties = {\n  margin: 0,\n  fontFamily: SANS,\n  fontWeight: 500,\n  fontSize: 42,\n  lineHeight: 1.12,\n  color: \"#fafafa\",\n};\n\nconst paragraphStyle: CSSProperties = {\n  margin: 0,\n  fontFamily: SANS,\n  fontWeight: 400,\n  fontSize: 21,\n  lineHeight: 1.55,\n  color: \"rgba(255,255,255,0.72)\",\n  maxWidth: 470,\n};\n\n// ---------------------------------------------------------------------------\n// Split scene — text column + glass code window, alternating sides.\n// ---------------------------------------------------------------------------\nconst SplitScene: React.FC<{\n  textLeft: boolean;\n  heading: ReactNode;\n  paragraph: ReactNode;\n  extra?: ReactNode;\n  codeTitle: string;\n  code: string;\n}> = ({ textLeft, heading, paragraph, extra, codeTitle, code }) => {\n  const textCol = (\n    <div\n      key=\"text\"\n      style={{\n        flex: 1,\n        padding: \"0 56px\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        justifyContent: \"center\",\n        gap: 22,\n        alignItems: \"flex-start\",\n      }}\n    >\n      <Reveal delay={3} distance={18}>\n        <h2 style={headingStyle}>{heading}</h2>\n      </Reveal>\n      <Reveal delay={10} distance={14}>\n        <p style={paragraphStyle}>{paragraph}</p>\n      </Reveal>\n      {extra ? (\n        <Reveal delay={18} distance={12}>\n          {extra}\n        </Reveal>\n      ) : null}\n    </div>\n  );\n\n  const codeCol = (\n    <div\n      key=\"code\"\n      style={{\n        flex: 1,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <Reveal\n        delay={6}\n        distance={72}\n        blur={14}\n        duration={20}\n        direction={textLeft ? \"left\" : \"right\"}\n      >\n        <div style={{ position: \"relative\", width: 600, height: 430 }}>\n          <GlassCodeBlock\n            code={code}\n            title={codeTitle}\n            width={600}\n            height={430}\n            fontSize={13}\n            staggerFrames={2}\n          />\n        </div>\n      </Reveal>\n    </div>\n  );\n\n  return (\n    <AbsoluteFill style={{ flexDirection: \"row\", alignItems: \"center\" }}>\n      {textLeft ? [textCol, codeCol] : [codeCol, textCol]}\n    </AbsoluteFill>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// Scene 1 — Hook.\n// ---------------------------------------------------------------------------\nconst HookScene: React.FC = () => (\n  <AbsoluteFill\n    style={{\n      alignItems: \"center\",\n      justifyContent: \"center\",\n      flexDirection: \"column\",\n      gap: 44,\n    }}\n  >\n     <Reveal delay={14} distance={22} blur={12} duration={24}>\n      <h1\n        style={{\n          ...headingStyle,\n          fontSize: 56,\n          textAlign: \"center\",\n          lineHeight: 1.05,\n        }}\n      >\n        Save up to{\" \"}\n        <Mark color={GREEN}>\n          50%\n        </Mark>{\" \"}\n        on inference costs\n      </h1>\n    </Reveal>\n    <ProviderIcons size={116} baseDelay={2} />\n   \n  </AbsoluteFill>\n);\n\n// ---------------------------------------------------------------------------\n// Scene 2 — Subtitle + install.\n// ---------------------------------------------------------------------------\nconst InstallScene: React.FC = () => (\n  <AbsoluteFill\n    style={{\n      alignItems: \"center\",\n      justifyContent: \"center\",\n      flexDirection: \"column\",\n      gap: 26,\n      padding: \"0 120px\",\n    }}\n  >\n    <Reveal delay={4} distance={18}>\n      <h2\n        style={{\n          ...headingStyle,\n          fontSize: 52,\n          textAlign: \"center\",\n        }}\n      >\n        Unified batch API for AI providers.\n      </h2>\n    </Reveal>\n    <Reveal delay={14} distance={14}>\n      <p\n        style={{\n          ...paragraphStyle,\n          fontSize: 23,\n          maxWidth: 720,\n          textAlign: \"center\",\n          color: \"rgba(255,255,255,0.78)\",\n        }}\n      >\n        Process LLM requests in bulk with a single call for lower costs.\n        Processing, uploading, polling, and result parsing handled for you.\n      </p>\n    </Reveal>\n    <Reveal delay={26} distance={14}>\n      <InstallPill fontSize={24} />\n    </Reveal>\n  </AbsoluteFill>\n);\n\n// ---------------------------------------------------------------------------\n// Scene 5 extra — webhook delivery badge.\n// ---------------------------------------------------------------------------\nconst WebhookBadge: React.FC = () => (\n  <div\n    style={{\n      display: \"inline-flex\",\n      alignItems: \"center\",\n      gap: 10,\n      padding: \"9px 16px\",\n      borderRadius: 10,\n      background: \"rgba(10,10,10,0.5)\",\n      border: \"1px solid rgba(74,222,128,0.3)\",\n      fontFamily: MONO,\n      fontSize: 17,\n      color: \"#d4d4d8\",\n    }}\n  >\n    <span style={{ color: GREEN }}>▸</span>\n    <span style={{ color: \"#fafafa\" }}>batch.completed</span>\n    <span style={{ color: \"#71717a\" }}>→</span>\n    <span style={{ color: GREEN }}>webhook delivered</span>\n  </div>\n);\n\n// ---------------------------------------------------------------------------\n// Scene 7 — Final logo.\n// ---------------------------------------------------------------------------\nconst OutroScene: React.FC = () => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n  const wordmark = useBlurInTransition(\n    [{ at: 10, state: \"revealed\", duration: 26 }],\n    { direction: \"up\", distance: 20, blur: 18 },\n  );\n  const dot = spring({ frame: frame - 22, fps, config: { damping: 13 } });\n\n  return (\n    <AbsoluteFill\n      style={{\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        flexDirection: \"column\",\n        gap: 30,\n      }}\n    >\n      <ProviderIcons size={92} baseDelay={2} />\n      <BlurIn style={wordmark} display=\"block\">\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"baseline\",\n            justifyContent: \"center\",\n            fontFamily: SANS,\n            fontWeight: 800,\n            fontSize: 110,\n            letterSpacing: \"-0.05em\",\n            color: \"#fafafa\",\n          }}\n        >\n          batchwork\n          <span\n            style={{\n              display: \"inline-block\",\n              width: 16,\n              height: 16,\n              borderRadius: \"50%\",\n              background: GREEN,\n              marginLeft: 14,\n              transform: `scale(${dot})`,\n            }}\n          />\n        </div>\n      </BlurIn>\n      <Reveal delay={20} distance={14}>\n        <p\n          style={{\n            ...paragraphStyle,\n            fontSize: 24,\n            textAlign: \"center\",\n            maxWidth: 720,\n            color: \"rgba(255,255,255,0.74)\",\n          }}\n        >\n          Unified batch API for AI providers.\n        </p>\n      </Reveal>\n      <Reveal delay={30} distance={14}>\n        <InstallPill fontSize={24} />\n      </Reveal>\n    </AbsoluteFill>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// Code samples.\n// ---------------------------------------------------------------------------\nconst CODE_SUMMARIZE = `import { batch } from \"batchwork\";\n\nconst job = await batch({\n  model: \"anthropic/claude-opus-4-0\",\n  requests: docs.map((doc) => ({\n    customId: doc.id,\n    prompt: \\`Summarize: \\${doc.text}\\`,\n  })),\n});\n\nconsole.log(job.id, job.provider, job.status);`;\n\nconst CODE_MODELS = `import { openai } from \"@ai-sdk/openai\";\nimport { anthropic } from \"@ai-sdk/anthropic\";\n\n// Pass the AI SDK models you already use\nbatch({ model: openai.chat(\"gpt-5.5\"), requests });\nbatch({ model: anthropic(\"claude-opus-4-0\"), requests });\n\n// …as a plain provider/model string\nbatch({ model: \"openai/gpt-5.5\", requests });`;\n\nconst CODE_POLLER = `import { createBatchPoller, createMemoryStore } from \"batchwork/server\";\n\nconst poller = createBatchPoller({ store: createMemoryStore() });\n\nawait poller.track(job, {\n  webhookUrl: \"https://acme.com/webhooks/batch\",\n  secret: process.env.BATCH_WEBHOOK_SECRET,\n});\n\n// Run tick() on a schedule — one signed webhook per finished batch\nexport const GET = async () => Response.json(await poller.tick());`;\n\nconst CODE_BATCHES = `import { createBatchRoutes, createMemoryStore } from \"batchwork/next\";\n\nexport const batches = createBatchRoutes({\n  store: createMemoryStore(),\n  onComplete: async (event, results) => {\n    for await (const r of results) {\n      await db.results.upsert({ id: r.customId, text: r.text });\n    }\n  },\n});\n\n// app/api/batches/route.ts\nexport const { GET, POST } = batches;`;\n\n// ---------------------------------------------------------------------------\n// Custom transition presentations — slide / zoom with motion blur + scale so\n// each hand-off has real movement instead of a flat dissolve.\n// ---------------------------------------------------------------------------\ntype SlideDir = \"from-left\" | \"from-right\" | \"from-bottom\" | \"from-top\";\n\nconst KineticSlide: React.FC<\n  TransitionPresentationComponentProps<{ direction: SlideDir }>\n> = ({ children, presentationProgress, presentationDirection, passedProps }) => {\n  const { direction } = passedProps;\n  const entering = presentationDirection === \"entering\";\n\n  const p = interpolate(presentationProgress, [0, 1], [0, 1], {\n    easing: Easing.inOut(Easing.cubic),\n  });\n\n  const axis =\n    direction === \"from-left\" || direction === \"from-right\" ? \"x\" : \"y\";\n  const enterSign =\n    direction === \"from-right\" || direction === \"from-bottom\" ? 1 : -1;\n\n  // Entering travels off-screen → 0; exiting is pushed 0 → the opposite edge.\n  const offsetPct = entering ? enterSign * (1 - p) * 100 : -enterSign * p * 100;\n  const translate =\n    axis === \"x\" ? `translateX(${offsetPct}%)` : `translateY(${offsetPct}%)`;\n\n  // Speed cues: motion blur and a scale dip peak at the midpoint of the move.\n  const motion = Math.sin(p * Math.PI);\n  const blur = motion * 16;\n  const scale = 1 - motion * 0.1;\n\n  return (\n    <AbsoluteFill\n      style={{\n        transform: `${translate} scale(${scale})`,\n        filter: blur > 0.1 ? `blur(${blur}px)` : undefined,\n      }}\n    >\n      {children}\n    </AbsoluteFill>\n  );\n};\n\nconst kineticSlide = (\n  direction: SlideDir,\n): TransitionPresentation<{ direction: SlideDir }> => ({\n  component: KineticSlide,\n  props: { direction },\n});\n\nconst ZoomBlur: React.FC<\n  TransitionPresentationComponentProps<{ rise: number }>\n> = ({ children, presentationProgress, presentationDirection, passedProps }) => {\n  const { rise } = passedProps;\n  const entering = presentationDirection === \"entering\";\n\n  const p = interpolate(presentationProgress, [0, 1], [0, 1], {\n    easing: entering ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic),\n  });\n\n  // Entering rises up and scales from 0.84 → 1 while un-blurring; the outgoing\n  // scene pushes \"through\" the viewer — scaling past 1 and blurring out.\n  const style: React.CSSProperties = entering\n    ? {\n        opacity: p,\n        transform: `translateY(${(1 - p) * rise}px) scale(${0.84 + p * 0.16})`,\n        filter: p < 1 ? `blur(${(1 - p) * 20}px)` : undefined,\n      }\n    : {\n        opacity: 1 - p,\n        transform: `translateY(${-p * rise}px) scale(${1 + p * 0.22})`,\n        filter: p > 0 ? `blur(${p * 20}px)` : undefined,\n      };\n\n  return <AbsoluteFill style={style}>{children}</AbsoluteFill>;\n};\n\nconst zoomBlur = (rise = 0): TransitionPresentation<{ rise: number }> => ({\n  component: ZoomBlur,\n  props: { rise },\n});\n\n// ---------------------------------------------------------------------------\n// Composition root.\n// ---------------------------------------------------------------------------\nexport const BatchworkDemo: React.FC = () => {\n  return (\n    <RemocnUIProvider>\n      <AbsoluteFill\n        style={\n          {\n            \"--font-geist-sans\": SANS_FAMILY,\n            \"--font-geist-mono\": MONO_FAMILY,\n          } as React.CSSProperties\n        }\n      >\n        {/* Persistent image background for the whole video. */}\n        <Backdrop fill={{ type: \"image\", src: demoAsset(\"bg.png\") }} />\n        {/* Scrim to deepen contrast for foreground content. */}\n        <AbsoluteFill\n          style={{\n            background:\n              \"radial-gradient(120% 120% at 50% 40%, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.5) 100%)\",\n          }}\n        />\n\n        <TransitionSeries>\n          <TransitionSeries.Sequence durationInFrames={S1}>\n            <HookScene />\n          </TransitionSeries.Sequence>\n\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: TI })}\n            presentation={zoomBlur()}\n          />\n\n          <TransitionSeries.Sequence durationInFrames={S2}>\n            <InstallScene />\n          </TransitionSeries.Sequence>\n\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: TK })}\n            presentation={kineticSlide(\"from-right\")}\n          />\n\n          <TransitionSeries.Sequence durationInFrames={S3}>\n            <SplitScene\n              textLeft\n              heading={\n                <>\n                  One API for every{\" \"}\n                  <Mark color={GREEN} after=\".\">\n                    batch provider\n                  </Mark>\n                </>\n              }\n              paragraph=\"Submit thousands of LLM requests with a single call — at roughly half the cost. Batchwork handles JSONL, file uploads, polling, and result parsing for you.\"\n              codeTitle=\"summarize.ts\"\n              code={CODE_SUMMARIZE}\n            />\n          </TransitionSeries.Sequence>\n\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: TK })}\n            presentation={kineticSlide(\"from-left\")}\n          />\n\n          <TransitionSeries.Sequence durationInFrames={S4}>\n            <SplitScene\n              textLeft={false}\n              heading={\n                <>\n                  Works with any{\" \"}\n                  <Mark color={GREEN} after=\".\">\n                    AI SDK model\n                  </Mark>\n                </>\n              }\n              paragraph=\"Author requests in the same generateText shape you already know. Pass any AI SDK model — swap a single line to change providers.\"\n              codeTitle=\"models.ts\"\n              code={CODE_MODELS}\n            />\n          </TransitionSeries.Sequence>\n\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: TK })}\n            presentation={kineticSlide(\"from-right\")}\n          />\n\n          <TransitionSeries.Sequence durationInFrames={S5}>\n            <SplitScene\n              textLeft\n              heading={\n                <>\n                  Automatic{\" \"}\n                  <Mark color={YELLOW}>\n                    polling\n                  </Mark>{\" \"}\n                  &amp;{\" \"}\n                  <Mark color={YELLOW} after=\".\">\n                    webhooks\n                  </Mark>\n                </>\n              }\n              paragraph=\"Register a job once and Batchwork polls open batches for you, delivering one signed webhook when each finishes — using OpenAI's native webhooks where they exist.\"\n              extra={<WebhookBadge />}\n              codeTitle=\"lib/poller.ts\"\n              code={CODE_POLLER}\n            />\n          </TransitionSeries.Sequence>\n\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: TK })}\n            presentation={kineticSlide(\"from-left\")}\n          />\n\n          <TransitionSeries.Sequence durationInFrames={S6}>\n            <SplitScene\n              textLeft={false}\n              heading={\n                <>\n                  Drop straight into{\" \"}\n                  <Mark color={GREEN} after=\".\">\n                    Next.js\n                  </Mark>\n                </>\n              }\n              paragraph=\"Export App Router route handlers for cron ticks and native webhooks. onComplete runs in-process, so results land straight in your database.\"\n              codeTitle=\"lib/batches.ts\"\n              code={CODE_BATCHES}\n            />\n          </TransitionSeries.Sequence>\n\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: TO })}\n            presentation={zoomBlur(70)}\n          />\n\n          <TransitionSeries.Sequence durationInFrames={S7}>\n            <OutroScene />\n          </TransitionSeries.Sequence>\n        </TransitionSeries>\n      </AbsoluteFill>\n    </RemocnUIProvider>\n  );\n};\n",
      "type": "registry:component",
      "target": "demos/batchwork/index.tsx"
    },
    {
      "path": "src/lib/demo-assets.ts",
      "content": "import { staticFile } from \"remotion\";\n\n// Demo assets are loaded from absolute URLs so a demo installed into another\n// project via the registry renders identically without copying this repo's\n// public/ directory. The same URLs are used on the site and in local renders,\n// which keeps a single code path (network access is required for rendering).\n//\n// Override with REMOTION_DEMO_ASSETS_BASE — only REMOTION_-prefixed env vars\n// reach Remotion compositions (next.config.js additionally inlines it for the\n// site's <Player>). Two override forms:\n//   - \"local\" — serve straight from this repo's public/ via staticFile();\n//     works in Remotion Studio, CLI renders and the Next dev site. Set it in\n//     the gitignored .env so previews never depend on pushed assets.\n//   - any URL — e.g. a local static server (http://127.0.0.1:8123).\nexport const DEMO_ASSETS_BASE =\n  process.env.REMOTION_DEMO_ASSETS_BASE ||\n  \"https://raw.githubusercontent.com/Remocn/remocn-collections/main/public\";\n\nexport const demoAsset = (path: string): string => {\n  const clean = path.replace(/^\\/+/, \"\");\n  if (DEMO_ASSETS_BASE === \"local\") return staticFile(clean);\n  return `${DEMO_ASSETS_BASE}/${clean}`;\n};\n",
      "type": "registry:component",
      "target": "lib/demo-assets.ts"
    },
    {
      "path": "src/demos/batchwork/prompt.md",
      "content": "<!-- TODO(draft): placeholder written by AI — replace with the real prompt used to generate this video -->\n\nMake a short product spot for batchwork — it's one batch API that works across every AI provider instead of juggling each provider's own batch endpoint. Lean on glass code windows to show the actual API usage and use marker-style highlights to call out the parts that matter (one client, swap the provider, same shape). Keep it tight and code-forward rather than heavy on marketing copy. Build it with remocn's glass code block and highlight components.\n",
      "type": "registry:file",
      "target": "demos/batchwork/prompt.md"
    }
  ],
  "docs": "Register the composition in your Remotion Root:\n\n  import { BatchworkDemo } from \"@/demos/batchwork\";\n  <Composition id=\"batchwork\" component={BatchworkDemo} durationInFrames={480} fps={30} width={1280} height={720} />\n\nRequires Tailwind v4 wired into Remotion (@remotion/tailwind-v4).\nRender locally: npx remotion render batchwork out/batchwork.mp4",
  "type": "registry:block"
}