{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stage",
  "title": "Stage",
  "description": "Place a screenshot, recording, or long page on a perspective studio plane and direct an integrated camera across it with timeline keyframes.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/scene-motion"],
  "files": [
    {
      "path": "registry/remocn/stage/index.tsx",
      "content": "\"use client\";\n\nimport type { CSSProperties, ReactNode } from \"react\";\nimport {\n  AbsoluteFill,\n  type EasingFunction,\n  interpolate,\n  random,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { CLAMP, EXPO, SETTLE_SOFT } from \"@/lib/remocn/scene-motion\";\n\nexport interface StageKey {\n  at: number;\n  x?: number;\n  y?: number;\n  zoom?: number;\n  rotate?: number;\n  easing?: EasingFunction;\n}\n\nexport interface StageContentSize {\n  width: number;\n  height: number;\n}\n\nexport interface StageProps {\n  children: ReactNode;\n  contentSize?: StageContentSize;\n  moves?: StageKey[];\n  shake?: number;\n  seed?: string;\n  backdrop?: string;\n  rotateX?: number;\n  rotateY?: number;\n  perspective?: number;\n  scale?: number;\n  radius?: number;\n  reflection?: number;\n  shadow?: number;\n  light?: number;\n  className?: string;\n}\n\nexport interface StagePose {\n  x: number;\n  y: number;\n  zoom: number;\n  rotate: number;\n}\n\nexport interface StageShake {\n  x: number;\n  y: number;\n  rotate: number;\n}\n\nexport interface StagePlaneGeometry {\n  width: number;\n  height: number;\n  left: number;\n  top: number;\n  targetX: number;\n  targetY: number;\n  translateX: number;\n  translateY: number;\n}\n\nconst NEUTRAL_POSE: StagePose = { x: 0.5, y: 0.5, zoom: 1, rotate: 0 };\n\nconst clamp01 = (value: number) => Math.min(1, Math.max(0, value));\n\nconst poseFromKey = (key: StageKey): StagePose => ({\n  x: clamp01(key.x ?? NEUTRAL_POSE.x),\n  y: clamp01(key.y ?? NEUTRAL_POSE.y),\n  zoom: key.zoom ?? NEUTRAL_POSE.zoom,\n  rotate: key.rotate ?? NEUTRAL_POSE.rotate,\n});\n\nexport function normalizeStageKeys(moves: StageKey[]): StageKey[] {\n  const byFrame = new Map<number, StageKey>();\n  for (const move of moves) byFrame.set(move.at, move);\n  return [...byFrame.values()].sort((a, b) => a.at - b.at);\n}\n\nexport function resolveStagePose(frame: number, moves: StageKey[]): StagePose {\n  const keys = normalizeStageKeys(moves);\n  if (keys.length === 0) return NEUTRAL_POSE;\n  if (frame <= keys[0].at) return poseFromKey(keys[0]);\n  if (frame >= keys[keys.length - 1].at) {\n    return poseFromKey(keys[keys.length - 1]);\n  }\n\n  const rightIndex = keys.findIndex((key) => frame <= key.at);\n  const left = keys[rightIndex - 1];\n  const right = keys[rightIndex];\n  const progress = interpolate(frame, [left.at, right.at], [0, 1], {\n    ...CLAMP,\n    easing: right.easing ?? EXPO,\n  });\n  const from = poseFromKey(left);\n  const to = poseFromKey(right);\n  const lerp = (a: number, b: number) => a + (b - a) * progress;\n\n  return {\n    x: lerp(from.x, to.x),\n    y: lerp(from.y, to.y),\n    zoom: lerp(from.zoom, to.zoom),\n    rotate: lerp(from.rotate, to.rotate),\n  };\n}\n\nconst smoothstep = (value: number) => value * value * (3 - 2 * value);\n\nfunction smoothNoise(frame: number, seed: string, axis: string): number {\n  const sample = frame / 6;\n  const index = Math.floor(sample);\n  const mix = smoothstep(sample - index);\n  const a = random(`${seed}-${axis}-${index}`) * 2 - 1;\n  const b = random(`${seed}-${axis}-${index + 1}`) * 2 - 1;\n  return a + (b - a) * mix;\n}\n\nexport function getStageShake(\n  frame: number,\n  shake: number,\n  seed = \"remocn-stage\",\n): StageShake {\n  const amount = clamp01(shake);\n  if (amount === 0) return { x: 0, y: 0, rotate: 0 };\n\n  const phase = random(`${seed}-phase`) * Math.PI * 2;\n  const xWave = Math.sin(frame * 0.19 + phase) * 0.58;\n  const yWave = Math.sin(frame * 0.13 + phase * 1.37) * 0.5;\n  const xFine = Math.sin(frame * 0.047 + phase * 0.71) * 0.22;\n  const yFine = Math.sin(frame * 0.071 + phase * 1.91) * 0.2;\n\n  return {\n    x: (xWave + xFine + smoothNoise(frame, seed, \"x\") * 0.32) * 7 * amount,\n    y: (yWave + yFine + smoothNoise(frame, seed, \"y\") * 0.3) * 5 * amount,\n    rotate:\n      (Math.sin(frame * 0.083 + phase * 0.43) * 0.65 +\n        smoothNoise(frame, seed, \"roll\") * 0.35) *\n      0.28 *\n      amount,\n  };\n}\n\nexport function getStagePlaneGeometry(\n  compositionWidth: number,\n  compositionHeight: number,\n  contentSize: StageContentSize | undefined,\n  pose: StagePose,\n  widthRatio = 0.84,\n): StagePlaneGeometry {\n  const safeContentWidth =\n    contentSize && contentSize.width > 0 ? contentSize.width : compositionWidth;\n  const safeContentHeight =\n    contentSize && contentSize.height > 0\n      ? contentSize.height\n      : compositionHeight;\n  const width = compositionWidth * widthRatio;\n  const height = width * (safeContentHeight / safeContentWidth);\n  const left = (compositionWidth - width) / 2;\n  const top = (compositionHeight - height) / 2;\n  const targetX = clamp01(pose.x) * width;\n  const targetY = clamp01(pose.y) * height;\n\n  return {\n    width,\n    height,\n    left,\n    top,\n    targetX,\n    targetY,\n    translateX: compositionWidth / 2 - (left + targetX),\n    translateY: compositionHeight / 2 - (top + targetY),\n  };\n}\n\nexport function getStageTransform(\n  geometry: StagePlaneGeometry,\n  pose: StagePose,\n  options: {\n    scale: number;\n    rotateX: number;\n    rotateY: number;\n    entryY?: number;\n    shake?: StageShake;\n  },\n): string {\n  const handheld = options.shake ?? { x: 0, y: 0, rotate: 0 };\n  const translateX = geometry.translateX - handheld.x;\n  const translateY = geometry.translateY + (options.entryY ?? 0) - handheld.y;\n  const rotateZ = -pose.rotate - handheld.rotate;\n\n  return `translate3d(${translateX}px, ${translateY}px, 0) scale(${options.scale * pose.zoom}) rotateZ(${rotateZ}deg) rotateX(${options.rotateX}deg) rotateY(${options.rotateY}deg)`;\n}\n\nexport function Stage({\n  children,\n  contentSize,\n  moves = [],\n  shake = 0,\n  seed = \"remocn-stage\",\n  backdrop = \"linear-gradient(145deg, #17181d 0%, #09090b 72%)\",\n  rotateX = 14,\n  rotateY = -20,\n  perspective = 900,\n  scale = 0.86,\n  radius = 1.4,\n  reflection = 0.24,\n  shadow = 0.7,\n  light = 0.55,\n  className,\n}: StageProps) {\n  const frame = useCurrentFrame();\n  const { width, height } = useVideoConfig();\n  const settle = interpolate(frame, [0, 24], [0, 1], {\n    ...CLAMP,\n    easing: SETTLE_SOFT,\n  });\n  const opacity = interpolate(frame, [0, 9], [0, 1], CLAMP);\n  const pose = resolveStagePose(frame, moves);\n  const handheld = getStageShake(frame, shake, seed);\n  const geometry = getStagePlaneGeometry(width, height, contentSize, pose);\n  const radiusPx = (radius / 100) * width;\n  const reflectionStrength = clamp01(reflection);\n  const shadowStrength = clamp01(shadow);\n  const lightStrength = clamp01(light);\n  const currentScale = interpolate(settle, [0, 1], [scale * 0.94, scale]);\n  const currentRotateX = interpolate(settle, [0, 1], [rotateX + 8, rotateX]);\n  const currentRotateY = interpolate(settle, [0, 1], [rotateY - 5, rotateY]);\n  const entryY = interpolate(settle, [0, 1], [64, 0]);\n  const transform = getStageTransform(geometry, pose, {\n    scale: currentScale,\n    rotateX: currentRotateX,\n    rotateY: currentRotateY,\n    entryY,\n    shake: handheld,\n  });\n\n  const planeStyle: CSSProperties = {\n    position: \"absolute\",\n    left: geometry.left,\n    top: geometry.top,\n    width: geometry.width,\n    height: geometry.height,\n    overflow: \"hidden\",\n    borderRadius: radiusPx,\n    transform,\n    transformOrigin: `${geometry.targetX}px ${geometry.targetY}px`,\n    transformStyle: \"preserve-3d\",\n    backfaceVisibility: \"visible\",\n    willChange: \"transform\",\n  };\n\n  return (\n    <AbsoluteFill\n      className={className}\n      style={{ overflow: \"hidden\", background: backdrop }}\n    >\n      <div\n        aria-hidden\n        style={{\n          position: \"absolute\",\n          left: \"13%\",\n          top: \"72%\",\n          width: \"74%\",\n          height: \"30%\",\n          opacity: reflectionStrength * opacity,\n          background:\n            \"linear-gradient(to bottom, rgba(188,181,255,0.32), rgba(91,81,145,0.1) 35%, transparent 76%)\",\n          clipPath: \"polygon(7% 0, 93% 0, 78% 100%, 22% 100%)\",\n          filter: \"blur(18px)\",\n          transform: `perspective(650px) rotateX(58deg) translateY(${entryY * 0.28}px)`,\n          transformOrigin: \"center top\",\n          pointerEvents: \"none\",\n        }}\n      />\n\n      <div\n        aria-hidden\n        style={{\n          position: \"absolute\",\n          left: \"16%\",\n          top: \"70%\",\n          width: \"68%\",\n          height: \"11%\",\n          borderRadius: \"50%\",\n          background: `rgba(0, 0, 0, ${0.52 * shadowStrength * opacity})`,\n          filter: `blur(${20 + 30 * shadowStrength}px)`,\n          transform: `translateY(${entryY * 0.35}px)`,\n          pointerEvents: \"none\",\n        }}\n      />\n\n      <AbsoluteFill\n        style={{\n          perspective,\n          perspectiveOrigin: \"50% 42%\",\n          opacity,\n          pointerEvents: \"none\",\n        }}\n      >\n        <div style={planeStyle}>{children}</div>\n      </AbsoluteFill>\n\n      <AbsoluteFill\n        aria-hidden\n        style={{\n          pointerEvents: \"none\",\n          opacity: lightStrength,\n          background:\n            \"radial-gradient(ellipse at 38% 7%, rgba(255,255,255,0.2), transparent 36%), linear-gradient(115deg, rgba(255,255,255,0.08), transparent 38%, rgba(255,255,255,0.02) 68%, rgba(0,0,0,0.24))\",\n          mixBlendMode: \"screen\",\n        }}\n      />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/stage.tsx"
    }
  ],
  "type": "registry:component"
}
