{
  "name": "transition-timing",
  "type": "registry:lib",
  "description": "Shared TransitionSeries timing helpers and layered progress curves",
  "dependencies": [
    "remotion",
    "@remotion/transitions"
  ],
  "registryDependencies": [
    "springs"
  ],
  "files": [
    {
      "path": "registry/bases/default/lib/transition-timing.ts",
      "type": "registry:lib",
      "content": "import { linearTiming, springTiming } from \"@remotion/transitions\";\nimport type { TransitionPresentationComponentProps } from \"@remotion/transitions\";\nimport { Easing, interpolate } from \"remotion\";\nimport { springSmooth } from \"./springs\";\n\nexport type TransitionVariant = \"linear\" | \"spring\" | \"editorial\";\n\n/** Balanced ease-in-out — matches `EASING.editorial` from `motion-tokens`. */\nconst EASING_CUT = Easing.bezier(0.45, 0, 0.55, 1);\n\nexport function resolveTransitionTiming({\n  durationInFrames,\n  variant = \"editorial\",\n}: {\n  durationInFrames: number;\n  variant?: TransitionVariant;\n}) {\n  if (variant === \"spring\") {\n    return springTiming({ config: springSmooth, durationInFrames });\n  }\n\n  return linearTiming({\n    durationInFrames,\n    // A cut is not an entrance. `EASING_ENTER` is tuned for an element landing\n    // on a stage that is already there and reaches ~0.9 by 40% of its window —\n    // used as a transition curve it spends three quarters of the overlap with\n    // both scenes already settled, which is why every presentation looked like\n    // a hard cut. An ease-in-out carries the frame across the whole window.\n    easing: variant === \"editorial\" ? EASING_CUT : undefined,\n  });\n}\n\nexport type TransitionDirection =\n  TransitionPresentationComponentProps<\n    Record<string, unknown>\n  >[\"presentationDirection\"];\n\nexport type TransitionPhase = {\n  isEntering: boolean;\n  /**\n   * How far the scene sits from its resting position, 0→1.\n   *\n   * `0` **always** means \"untouched\": the scene is whole, centred and sharp.\n   * `1` means fully displaced — off-stage for an entering scene that has not\n   * arrived yet, and off-stage again for an exiting scene that has left.\n   *\n   * A presentation therefore only ever scales its effect by `displace`, and\n   * never has to know which direction it is playing. This matters because\n   * `TransitionSeries` keeps a presentation mounted for the sequence's whole\n   * life and holds `presentationProgress` at 0 outside the overlap — so any\n   * effect that is non-zero at `displace === 0` is applied to the scene for\n   * every frame it is on screen, not just during the transition.\n   */\n  displace: number;\n  /** 1 = opaque. Only meaningful when the presentation opted into fading. */\n  opacity: number;\n  /** Untouched 0→1 progress of the transition itself, both directions. */\n  progress: number;\n};\n\nexport type TransitionPhaseOptions = {\n  /**\n   * Fraction of the window the displacement takes, so motion can settle before\n   * the transition ends (entering) or hold before it starts (exiting).\n   */\n  lead?: number;\n  /**\n   * Crossfade alongside the displacement. Leave `false` for wipes and any\n   * presentation that covers the frame — fading both scenes at once lets the\n   * background show through the middle of the cut.\n   */\n  fade?: boolean;\n};\n\n/**\n * Normalises a presentation's progress into a direction-agnostic phase.\n *\n * ```tsx\n * const { displace, opacity } = transitionPhase(\n *   presentationProgress,\n *   presentationDirection,\n *   { fade: true },\n * );\n * const blur = displace * maxBlur; // 0 when the scene is at rest\n * ```\n */\nexport function transitionPhase(\n  presentationProgress: number,\n  presentationDirection: TransitionDirection,\n  { lead = 0.72, fade = false }: TransitionPhaseOptions = {},\n): TransitionPhase {\n  const isEntering = presentationDirection === \"entering\";\n\n  // No easing here on purpose. `presentationProgress` has already been shaped\n  // by the transition's `timing`; easing it a second time compresses the whole\n  // move into the first few frames of the overlap and every presentation\n  // degenerates into a hard cut. This only re-windows the progress.\n  // `lead >= 1` means the scene moves across the whole window with no settle.\n  // Presentations where both scenes travel together (a push) need this — a\n  // lead below 1 desynchronises them and opens a gap onto the background.\n  const spansWindow = lead >= 1;\n\n  if (isEntering) {\n    const arrived = spansWindow\n      ? presentationProgress\n      : interpolate(presentationProgress, [0, lead, 1], [0, 1, 1], {\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n        });\n\n    return {\n      isEntering,\n      displace: 1 - arrived,\n      opacity: fade\n        ? interpolate(presentationProgress, [0, 0.42, 1], [0, 0.72, 1], {\n            extrapolateLeft: \"clamp\",\n            extrapolateRight: \"clamp\",\n          })\n        : 1,\n      progress: presentationProgress,\n    };\n  }\n\n  const left = spansWindow\n    ? presentationProgress\n    : interpolate(presentationProgress, [0, 1 - lead, 1], [0, 0, 1], {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n      });\n\n  return {\n    isEntering,\n    displace: left,\n    opacity: fade\n      ? interpolate(presentationProgress, [0, 0.58, 1], [1, 0.42, 0], {\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n        })\n      : 1,\n    progress: presentationProgress,\n  };\n}\n"
    }
  ]
}