{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hyperspace",
  "title": "Hyperspace",
  "description": "A deterministic starfield stretches into radial streaks, the scene tunnels forward and a white flash at the cut reveals the next scene.",
  "dependencies": ["remotion", "@remotion/transitions"],
  "files": [
    {
      "path": "registry/remocn/hyperspace/index.tsx",
      "content": "\"use client\";\n\nimport type {\n  TransitionPresentation,\n  TransitionPresentationComponentProps,\n} from \"@remotion/transitions\";\nimport type React from \"react\";\nimport { useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  random,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type HyperspaceProps = {\n  /** Number of stars in the field. */\n  stars?: number;\n  /** Main streak color. */\n  streakColor?: string;\n  /** Color of the occasional tinted streak. */\n  accentColor?: string;\n  /** Share of stars drawn in `accentColor`, 0 to 1. */\n  accentRatio?: number;\n  /** White flash at the cut. Set false for a hard cut through the streaks. */\n  flash?: boolean;\n  /** Flash color. */\n  flashColor?: string;\n};\n\nexport const hyperspaceDefaults: Required<HyperspaceProps> = {\n  stars: 260,\n  streakColor: \"#FFFFFF\",\n  accentColor: \"#E5322D\",\n  accentRatio: 0.14,\n  flash: true,\n  flashColor: \"#FFFFFF\",\n};\n\nconst clamp = {\n  extrapolateLeft: \"clamp\" as const,\n  extrapolateRight: \"clamp\" as const,\n};\n\n// Log-radius travel. Stars live on rays from the center and their radius grows\n// as exp(travel), which reads as perspective: slow near the vanishing point,\n// rushing at the edges.\nconst JUMP = 3.2;\nconst COAST = 1.1;\nconst CUT = 0.62;\n// Slow drift the stars cover before the jump kicks in.\nconst SPOOL = 0.22;\nconst MAX_TAIL = 0.94;\n\ntype Star = { angle: number; logR: number; accent: boolean; weight: number };\n\nfunction travelAt(p: number) {\n  if (p <= CUT) {\n    // Spool up: a slow linear drift that hands over to a quartic surge, so the\n    // field starts moving gently instead of lurching.\n    const t = p / CUT;\n    return { travel: SPOOL * t + (JUMP - SPOOL) * t ** 4, speed: t ** 3 };\n  }\n  const q = (p - CUT) / (1 - CUT);\n  const ease = 1 - (1 - q) ** 2;\n  return { travel: JUMP + COAST * ease, speed: 0.7 * (1 - q) };\n}\n\nfunction Starfield({\n  stars,\n  progress,\n  streakColor,\n  accentColor,\n  opacity,\n  stride,\n}: {\n  stars: Star[];\n  progress: number;\n  streakColor: string;\n  accentColor: string;\n  opacity: number;\n  stride: number;\n}) {\n  const { width, height } = useVideoConfig();\n  const cx = width / 2;\n  const cy = height / 2;\n  const diag = Math.hypot(width, height) / 2;\n  const unit = Math.min(width, height) / 720;\n  const { travel, speed } = travelAt(progress);\n  // Streak length grows with speed squared.\n  const tail = Math.min(MAX_TAIL, 0.006 + MAX_TAIL * speed ** 2);\n\n  const lines: React.ReactNode[] = [];\n  stars.forEach((star, i) => {\n    if (i % stride !== 0) return;\n    const r = Math.exp(star.logR + travel);\n    const r0 = r * (1 - tail);\n    if (r0 > diag * 1.05) return;\n    const near = Math.min(1, r / (diag * 0.12));\n    const alpha = (0.25 + 0.75 * near) * opacity;\n    const cos = Math.cos(star.angle);\n    const sin = Math.sin(star.angle);\n    lines.push(\n      <line\n        key={i}\n        x1={cx + cos * r0}\n        y1={cy + sin * r0}\n        x2={cx + cos * r}\n        y2={cy + sin * r}\n        stroke={star.accent ? accentColor : streakColor}\n        strokeOpacity={alpha}\n        strokeWidth={(0.8 + star.weight * 2.4 * near) * unit}\n        strokeLinecap=\"round\"\n      />,\n    );\n  });\n\n  return (\n    <svg\n      width={width}\n      height={height}\n      viewBox={`0 0 ${width} ${height}`}\n      style={{ position: \"absolute\", inset: 0 }}\n      aria-hidden\n    >\n      {lines}\n    </svg>\n  );\n}\n\nconst HyperspacePresentation: React.FC<\n  TransitionPresentationComponentProps<HyperspaceProps>\n> = ({\n  children,\n  presentationProgress,\n  presentationDirection,\n  passedProps,\n}) => {\n  const {\n    stars: count,\n    streakColor,\n    accentColor,\n    accentRatio,\n    flash,\n    flashColor,\n  } = { ...hyperspaceDefaults, ...passedProps };\n  const { width, height } = useVideoConfig();\n  const diag = Math.hypot(width, height) / 2;\n  const p = presentationProgress;\n\n  const stars = useMemo<Star[]>(() => {\n    const minLog = Math.log(Math.max(2, diag * 0.015));\n    const maxLog = Math.log(diag);\n    return Array.from({ length: Math.max(0, Math.round(count)) }, (_, i) => ({\n      angle: random(`hyperspace-a-${i}`) * Math.PI * 2,\n      logR: minLog + random(`hyperspace-r-${i}`) * (maxLog - minLog),\n      accent: random(`hyperspace-c-${i}`) < accentRatio,\n      weight: random(`hyperspace-w-${i}`),\n    }));\n  }, [count, accentRatio, diag]);\n\n  if (presentationDirection === \"exiting\") {\n    const t = Math.min(1, p / CUT);\n    // Quartic push: barely moves while the stars spool up, then surges.\n    const push = 1 + 0.42 * t ** 4;\n    const dim = interpolate(t, [0, 1], [1, 0.45], {\n      ...clamp,\n      easing: Easing.in(Easing.quad),\n    });\n    const fieldIn = interpolate(t, [0, 0.45], [0, 1], {\n      ...clamp,\n      easing: Easing.inOut(Easing.quad),\n    });\n    const white = flash\n      ? interpolate(t, [0.72, 1], [0, 1], {\n          ...clamp,\n          easing: Easing.in(Easing.quad),\n        })\n      : 0;\n    return (\n      <AbsoluteFill style={{ backgroundColor: \"#000000\", overflow: \"hidden\" }}>\n        <AbsoluteFill\n          style={{ transform: `scale(${push})`, filter: `brightness(${dim})` }}\n        >\n          {children}\n        </AbsoluteFill>\n        <Starfield\n          stars={stars}\n          progress={Math.min(p, CUT)}\n          streakColor={streakColor}\n          accentColor={accentColor}\n          opacity={fieldIn}\n          stride={1}\n        />\n        <AbsoluteFill style={{ backgroundColor: flashColor, opacity: white }} />\n      </AbsoluteFill>\n    );\n  }\n\n  if (p < CUT) {\n    return <AbsoluteFill style={{ opacity: 0 }}>{children}</AbsoluteFill>;\n  }\n\n  // Arrival: the flash holds for a beat, then dissolves slowly while the scene\n  // eases out of a slight zoom and soft focus, so the landing feels calm\n  // after the jump.\n  const q = (p - CUT) / (1 - CUT);\n  const unit = Math.min(width, height);\n  const soft = Easing.bezier(0.22, 1, 0.36, 1);\n  const settle = interpolate(q, [0, 1], [1.06, 1], { ...clamp, easing: soft });\n  const blur = interpolate(q, [0, 0.75], [unit * 0.018, 0], {\n    ...clamp,\n    easing: soft,\n  });\n  const white = flash\n    ? interpolate(q, [0, 0.1, 1], [1, 1, 0], {\n        ...clamp,\n        easing: Easing.inOut(Easing.sin),\n      })\n    : 0;\n  const fade = interpolate(q, [0, 0.45], [0.8, 0], {\n    ...clamp,\n    easing: Easing.out(Easing.quad),\n  });\n  return (\n    <AbsoluteFill style={{ overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          transform: `scale(${settle})`,\n          filter: blur > 0.1 ? `blur(${blur}px)` : undefined,\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n      <Starfield\n        stars={stars}\n        progress={p}\n        streakColor={streakColor}\n        accentColor={accentColor}\n        opacity={fade}\n        stride={5}\n      />\n      <AbsoluteFill style={{ backgroundColor: flashColor, opacity: white }} />\n    </AbsoluteFill>\n  );\n};\n\nexport function hyperspace(\n  props: HyperspaceProps = {},\n): TransitionPresentation<HyperspaceProps> {\n  return { component: HyperspacePresentation, props };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/hyperspace.tsx"
    }
  ],
  "type": "registry:component"
}
