{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spring-settle",
  "title": "Spring Settle",
  "description": "A scene sequencer where the outgoing scene shrinks away as one group, the stage holds empty for a beat, and the next scene's items land staggered from above-1 scale.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/scene-motion"],
  "files": [
    {
      "path": "registry/remocn/spring-settle/index.tsx",
      "content": "\"use client\";\n\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { createContext, useContext } from \"react\";\nimport {\n  AbsoluteFill,\n  interpolate,\n  interpolateColors,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { CLAMP, FADE, stagger } from \"@/lib/remocn/scene-motion\";\n\nexport type SpringSettleConfig = {\n  sceneFrames: number;\n  gapFrames: number;\n  enterScale: number;\n  enterFadeFrames: number;\n  enterStagger: number;\n  enterStaggerPower: number;\n  enterStaggerJitter: number;\n  springDamping: number;\n  springStiffness: number;\n  springMass: number;\n  exitFrames: number;\n  exitScale: number;\n  exitPower: number;\n  bgFadeFrames: number;\n};\n\nexport const SPRING_SETTLE_DEFAULTS: SpringSettleConfig = {\n  sceneFrames: 70,\n  gapFrames: 1,\n  enterScale: 1.24,\n  enterFadeFrames: 5,\n  enterStagger: 3,\n  enterStaggerPower: 0.8,\n  enterStaggerJitter: 1,\n  springDamping: 30,\n  springStiffness: 320,\n  springMass: 1,\n  exitFrames: 6,\n  exitScale: 0.84,\n  exitPower: 5,\n  bgFadeFrames: 4,\n};\n\nexport type SpringSettleSceneDef = {\n  name?: string;\n  bg?: string;\n  durationInFrames?: number;\n  content: ReactNode;\n};\n\nexport type SpringSettlePhase = \"enter\" | \"dwell\" | \"exit\" | \"gap\";\n\nexport type SpringSettleTimeline = {\n  index: number;\n  local: number;\n  dwellFrames: number;\n};\n\ntype SceneClock = {\n  local: number;\n  cfg: SpringSettleConfig;\n  fps: number;\n};\n\nconst SceneCtx = createContext<SceneClock | null>(null);\n\nconst resolveConfig = (\n  config?: Partial<SpringSettleConfig>,\n): SpringSettleConfig => ({ ...SPRING_SETTLE_DEFAULTS, ...config });\n\nconst dwellOf = (scene: SpringSettleSceneDef, cfg: SpringSettleConfig) =>\n  scene.durationInFrames ?? cfg.sceneFrames;\n\nexport const springSettleTimeline = (\n  frame: number,\n  scenes: SpringSettleSceneDef[],\n  config?: Partial<SpringSettleConfig>,\n  opts?: { loop?: boolean },\n): SpringSettleTimeline => {\n  const cfg = resolveConfig(config);\n  const lens = scenes.map((s) => dwellOf(s, cfg) + cfg.gapFrames);\n  const total = lens.reduce((a, b) => a + b, 0);\n  let w = opts?.loop ? ((frame % total) + total) % total : frame;\n  let index = 0;\n  while (index < scenes.length - 1 && w >= lens[index]) {\n    w -= lens[index];\n    index += 1;\n  }\n  const local = Math.min(w, lens[index] - 1);\n  return { index, local, dwellFrames: dwellOf(scenes[index], cfg) };\n};\n\nexport const springSettlePhase = (\n  timeline: SpringSettleTimeline,\n  fps: number,\n  config?: Partial<SpringSettleConfig>,\n): SpringSettlePhase => {\n  const cfg = resolveConfig(config);\n  const { local, dwellFrames } = timeline;\n  if (local >= dwellFrames) return \"gap\";\n  if (local >= dwellFrames - cfg.exitFrames) return \"exit\";\n  const settled =\n    spring({\n      frame: local,\n      fps,\n      config: {\n        damping: cfg.springDamping,\n        stiffness: cfg.springStiffness,\n        mass: cfg.springMass,\n      },\n    }) > 0.985;\n  return settled ? \"dwell\" : \"enter\";\n};\n\nexport const springSettleExitStyle = (\n  local: number,\n  dwellFrames: number,\n  config?: Partial<SpringSettleConfig>,\n): CSSProperties => {\n  const cfg = resolveConfig(config);\n  const tExit = interpolate(\n    local,\n    [dwellFrames - cfg.exitFrames, dwellFrames],\n    [0, 1],\n    CLAMP,\n  );\n  if (tExit <= 0) return {};\n  const eExit = tExit ** cfg.exitPower;\n  return {\n    scale: `${1 - (1 - cfg.exitScale) * eExit}`,\n    opacity: 1 - eExit,\n    willChange: \"transform, opacity\",\n  };\n};\n\nexport interface SpringSettleEnterProps {\n  local: number;\n  config?: Partial<SpringSettleConfig>;\n  children: ReactNode;\n}\n\nexport function SpringSettleEnter({\n  local,\n  config,\n  children,\n}: SpringSettleEnterProps) {\n  const { fps } = useVideoConfig();\n  return (\n    <SceneCtx.Provider value={{ local, cfg: resolveConfig(config), fps }}>\n      {children}\n    </SceneCtx.Provider>\n  );\n}\n\nexport interface SpringSettleItemProps {\n  index?: number;\n  style?: CSSProperties;\n  children: ReactNode;\n}\n\nexport function SpringSettleItem({\n  index = 0,\n  style,\n  children,\n}: SpringSettleItemProps) {\n  const clock = useContext(SceneCtx);\n  if (!clock) {\n    return <div style={style}>{children}</div>;\n  }\n  const { local, cfg, fps } = clock;\n  const tEnter =\n    local -\n    stagger(\n      index,\n      cfg.enterStagger,\n      cfg.enterStaggerPower,\n      cfg.enterStaggerJitter,\n    );\n  const enterP = spring({\n    frame: tEnter,\n    fps,\n    config: {\n      damping: cfg.springDamping,\n      stiffness: cfg.springStiffness,\n      mass: cfg.springMass,\n    },\n  });\n  const scaleEnter = interpolate(enterP, [0, 1], [cfg.enterScale, 1]);\n  const opacityEnter = interpolate(tEnter, [0, cfg.enterFadeFrames], [0, 1], {\n    ...CLAMP,\n    easing: FADE,\n  });\n  return (\n    <div\n      style={{\n        ...style,\n        scale: `${scaleEnter}`,\n        opacity: opacityEnter,\n        willChange: \"transform, opacity\",\n      }}\n    >\n      {children}\n    </div>\n  );\n}\n\nconst wrapSigned = (d: number, total: number) =>\n  (((d % total) + total * 1.5) % total) - total / 2;\n\nexport interface SpringSettleScenesProps {\n  scenes: SpringSettleSceneDef[];\n  config?: Partial<SpringSettleConfig>;\n  loop?: boolean;\n  style?: CSSProperties;\n}\n\nexport function SpringSettleScenes({\n  scenes,\n  config,\n  loop = false,\n  style,\n}: SpringSettleScenesProps) {\n  const frame = useCurrentFrame();\n  const cfg = resolveConfig(config);\n\n  const lens = scenes.map((s) => dwellOf(s, cfg) + cfg.gapFrames);\n  const total = lens.reduce((a, b) => a + b, 0);\n  const { index, local, dwellFrames } = springSettleTimeline(\n    frame,\n    scenes,\n    cfg,\n    { loop },\n  );\n\n  const n = scenes.length;\n  const cur = scenes[index];\n  const prev = loop ? scenes[(index - 1 + n) % n] : scenes[index - 1];\n  const next = loop ? scenes[(index + 1) % n] : scenes[index + 1];\n\n  const w = loop ? ((frame % total) + total) % total : frame;\n  const slotStart = w - local;\n  const half = Math.max(cfg.bgFadeFrames, 0.001) / 2;\n  let bg = cur.bg;\n  if (prev?.bg && cur.bg) {\n    let d = w - (slotStart - cfg.gapFrames / 2);\n    if (loop) d = wrapSigned(d, total);\n    if (Math.abs(d) <= half) {\n      bg = interpolateColors(d, [-half, half], [prev.bg, cur.bg]);\n    }\n  }\n  if (next?.bg && cur.bg && bg === cur.bg) {\n    let d = w - (slotStart + dwellFrames + cfg.gapFrames / 2);\n    if (loop) d = wrapSigned(d, total);\n    if (Math.abs(d) <= half) {\n      bg = interpolateColors(d, [-half, half], [cur.bg, next.bg]);\n    }\n  }\n\n  return (\n    <AbsoluteFill style={{ background: bg, ...style }}>\n      {local < dwellFrames ? (\n        <AbsoluteFill style={springSettleExitStyle(local, dwellFrames, cfg)}>\n          <SpringSettleEnter local={local} config={cfg}>\n            {cur.content}\n          </SpringSettleEnter>\n        </AbsoluteFill>\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/spring-settle.tsx"
    }
  ],
  "type": "registry:component"
}
