{
  "name": "perspective-marquee",
  "type": "registry:ui",
  "description": "Marquee tilted in 3D with depth fade",
  "dependencies": [
    "remotion"
  ],
  "registryDependencies": [
    "layout"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/perspective-marquee.tsx",
      "type": "registry:ui",
      "content": "import { measureText } from \"@remotion/layout-utils\";\nimport { useMemo } from \"react\";\nimport { useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { scaleFont } from \"@/remotion/lib/layout\";\n\nexport type PerspectiveMarqueeProps = {\n  text: string;\n  /** Scroll speed in px per frame along the floor plane. */\n  speed?: number;\n  fontSize?: number;\n  color?: string;\n  fontWeight?: number;\n  fontFamily?: string;\n  gap?: number;\n  /** Floor plane tilt in degrees — higher values exaggerate depth. */\n  floorTilt?: number;\n  /** Perspective distance in px — lower values exaggerate depth. */\n  perspective?: number;\n  showFloorGrid?: boolean;\n};\n\nfunction getItemWidth(\n  text: string,\n  fontSize: number,\n  fontWeight: number,\n  fontFamily?: string,\n) {\n  const family = fontFamily ?? \"system-ui\";\n\n  if (typeof document !== \"undefined\") {\n    return measureText({\n      text,\n      fontFamily: family,\n      fontSize,\n      fontWeight: String(fontWeight),\n    }).width;\n  }\n\n  return text.length * fontSize * 0.55;\n}\n\nexport const PerspectiveMarquee: React.FC<PerspectiveMarqueeProps> = ({\n  text,\n  speed = 10,\n  fontSize: fontSizeProp,\n  color = \"#f4f4f5\",\n  fontWeight = 600,\n  fontFamily,\n  gap = 72,\n  floorTilt = 70,\n  perspective = 640,\n  showFloorGrid = true,\n}) => {\n  const frame = useCurrentFrame();\n  const { width, height } = useVideoConfig();\n  const fontSize = fontSizeProp ?? scaleFont(56, width);\n  const farFontSize = Math.round(fontSize * 0.58);\n  const farGap = Math.round(gap * 1.1);\n  const displayText = text.toUpperCase();\n\n  const { repetitions, nearLoopWidth, loopPeriodFrames } = useMemo(() => {\n    const nearItemWidth = getItemWidth(\n      displayText,\n      fontSize,\n      fontWeight,\n      fontFamily,\n    );\n    const count = Math.max(4, Math.ceil(width / nearItemWidth) + 2);\n    const nearLoop =\n      count * nearItemWidth + count * gap;\n\n    return {\n      repetitions: count,\n      nearLoopWidth: nearLoop,\n      loopPeriodFrames: Math.max(1, Math.round(nearLoop / speed)),\n    };\n  }, [displayText, fontFamily, fontSize, fontWeight, gap, speed, width]);\n\n  const progress = (frame % loopPeriodFrames) / loopPeriodFrames;\n  const scrollX = -progress * 50;\n\n  const items = useMemo(\n    () =>\n      Array.from({ length: repetitions * 2 }, (_, i) => (\n        <span key={i}>{text}</span>\n      )),\n    [repetitions, text],\n  );\n\n  const horizonTop = Math.round(height * 0.34);\n  const planeHeight = Math.round(height * 0.9);\n  const gridCellW = Math.round(width * 0.11);\n  const gridCellH = Math.round(fontSize * 0.75);\n  const nearTrackBottom = Math.round(planeHeight * 0.38);\n  const farTrackBottom = Math.round(planeHeight * 0.56);\n\n  const trackTypography = {\n    fontWeight,\n    color,\n    whiteSpace: \"nowrap\" as const,\n    letterSpacing: \"0.05em\",\n    textTransform: \"uppercase\" as const,\n    ...(fontFamily ? { fontFamily } : {}),\n  };\n\n  return (\n    <div\n      style={{\n        width: \"100%\",\n        height: \"100%\",\n        overflow: \"hidden\",\n        position: \"relative\",\n        background: \"#080810\",\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background:\n            \"radial-gradient(ellipse 90% 45% at 50% 0%, rgba(255,255,255,0.09) 0%, transparent 70%)\",\n          pointerEvents: \"none\",\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          top: horizonTop,\n          left: \"5%\",\n          right: \"5%\",\n          height: 1,\n          background:\n            \"linear-gradient(90deg, transparent, rgba(255,255,255,0.55), transparent)\",\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          right: 0,\n          bottom: 0,\n          height: Math.round(height * 0.74),\n          perspective,\n          perspectiveOrigin: `50% ${horizonTop}px`,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: \"50%\",\n            bottom: 0,\n            width: \"280%\",\n            marginLeft: \"-140%\",\n            height: planeHeight,\n            transform: `rotateX(${floorTilt}deg)`,\n            transformOrigin: \"50% 100%\",\n            transformStyle: \"preserve-3d\",\n          }}\n        >\n          {showFloorGrid ? (\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                bottom: nearTrackBottom,\n                translate: `${scrollX}% 0`,\n              }}\n            >\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: \"-12%\",\n                  right: \"-12%\",\n                  top: \"-240%\",\n                  bottom: \"-45%\",\n                  backgroundImage: `\n                    linear-gradient(to right, rgba(255,255,255,0.2) 1px, transparent 1px),\n                    linear-gradient(to top, rgba(255,255,255,0.14) 1px, transparent 1px)\n                  `,\n                  backgroundSize: `${gridCellW}px ${gridCellH}px`,\n                  maskImage:\n                    \"linear-gradient(to top, black 8%, rgba(0,0,0,0.5) 62%, transparent 100%)\",\n                  WebkitMaskImage:\n                    \"linear-gradient(to top, black 8%, rgba(0,0,0,0.5) 62%, transparent 100%)\",\n                  pointerEvents: \"none\",\n                }}\n              />\n              <div style={{ width: nearLoopWidth * 2, height: 1, opacity: 0 }} />\n            </div>\n          ) : null}\n\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              bottom: farTrackBottom,\n              opacity: 0.32,\n            }}\n          >\n            <div\n              style={{\n                display: \"inline-flex\",\n                gap: farGap,\n                paddingLeft: gap,\n                translate: `${scrollX}% 0`,\n                fontSize: farFontSize,\n                ...trackTypography,\n              }}\n            >\n              {items}\n            </div>\n          </div>\n\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              bottom: nearTrackBottom,\n            }}\n          >\n            <div\n              style={{\n                display: \"inline-flex\",\n                gap,\n                paddingLeft: gap,\n                translate: `${scrollX}% 0`,\n                fontSize,\n                ...trackTypography,\n              }}\n            >\n              {items}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `linear-gradient(to bottom, rgba(8,8,16,0.92) 0%, transparent ${horizonTop + 52}px, transparent 85%, rgba(8,8,16,0.25) 100%)`,\n        }}\n      />\n    </div>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "atoms",
    "drive": "time",
    "tier": "advanced",
    "tags": [
      "text"
    ]
  }
}