{
  "name": "ai-generation-canvas",
  "type": "registry:block",
  "description": "Prompt-to-dashboard generation with morphing input, skeleton shimmer, and card flips",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "layout"
  ],
  "composition": {
    "id": "AiGenerationCanvas",
    "component": "AiGenerationCanvas",
    "durationInFrames": 180,
    "fps": 30,
    "width": 1920,
    "height": 1080,
    "importPath": "@/compositions/ai-generation-canvas/index"
  },
  "files": [
    {
      "path": "registry/bases/default/compositions/ai-generation-canvas/index.tsx",
      "type": "registry:block",
      "content": "import { loadFont } from \"@remotion/google-fonts/Inter\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { getSafeAreaPadding, scaleFont } from \"@/remotion/lib/layout\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"500\", \"600\", \"700\"],\n  subsets: [\"latin\"],\n});\n\n/** Frames over which the whole canvas fades out at the end of the demo. */\nconst EXIT_DURATION = 22;\nconst DEMO_BG = \"#080810\";\n\nexport type AiGenerationMetric = {\n  label: string;\n  value: string;\n  delta?: string;\n};\n\nexport type AiGenerationCanvasProps = {\n  prompt?: string;\n  accentColor?: string;\n  cardCount?: number;\n  metrics?: AiGenerationMetric[];\n  eyebrow?: string;\n  statusLabel?: string;\n  speed?: number;\n};\n\nconst DEFAULT_METRICS: AiGenerationMetric[] = [\n  { label: \"Revenue\", value: \"$48.2k\", delta: \"+12.4%\" },\n  { label: \"Active users\", value: \"12,840\", delta: \"+18.1%\" },\n  { label: \"Sessions\", value: \"9.1k\", delta: \"+8.7%\" },\n  { label: \"Conversion\", value: \"3.8%\", delta: \"+2.2%\" },\n  { label: \"Retention\", value: \"76%\", delta: \"+5.4%\" },\n  { label: \"Latency\", value: \"142ms\", delta: \"-11.8%\" },\n];\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value));\n}\n\nfunction fittedPromptSize(prompt: string, width: number, isPortrait: boolean) {\n  const base = scaleFont(isPortrait ? 46 : 34, width);\n  const longCopyAdjustment = prompt.length > 56 ? 0.78 : prompt.length > 36 ? 0.88 : 1;\n  return Math.round(clamp(base * longCopyAdjustment, scaleFont(24, width), base));\n}\n\nconst SparkIcon: React.FC<{ size: number; color: string }> = ({ size, color }) => (\n  <svg viewBox=\"0 0 24 24\" width={size} height={size} fill=\"none\">\n    <path\n      d=\"M12 2.6l2.58 6.82L21.4 12l-6.82 2.58L12 21.4l-2.58-6.82L2.6 12l6.82-2.58L12 2.6z\"\n      fill={color}\n    />\n  </svg>\n);\n\nconst MiniBarChart: React.FC<{\n  accentColor: string;\n  scale: number;\n  progress: number;\n}> = ({ accentColor, scale, progress }) => {\n  const bars = [0.42, 0.72, 0.56, 0.88, 0.5, 0.95, 0.66];\n\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"flex-end\",\n        gap: 8 * scale,\n        height: 76 * scale,\n        marginTop: 18 * scale,\n      }}\n    >\n      {bars.map((height, index) => (\n        <div\n          key={index}\n          style={{\n            flex: 1,\n            height: `${height * progress * 100}%`,\n            minHeight: 4 * scale,\n            background: `linear-gradient(180deg, ${accentColor}, ${accentColor}88)`,\n            borderRadius: 999,\n            opacity: 0.34 + height * 0.62,\n          }}\n        />\n      ))}\n    </div>\n  );\n};\n\nconst MiniRows: React.FC<{ scale: number; progress: number }> = ({\n  scale,\n  progress,\n}) => (\n  <div\n    style={{\n      marginTop: 18 * scale,\n      display: \"flex\",\n      flexDirection: \"column\",\n      gap: 8 * scale,\n    }}\n  >\n    {[0.9, 0.7, 0.84, 0.58].map((width, index) => (\n      <div\n        key={index}\n        style={{\n          height: 10 * scale,\n          width: `${width * progress * 100}%`,\n          background: \"rgba(255,255,255,0.14)\",\n          borderRadius: 999,\n        }}\n      />\n    ))}\n  </div>\n);\n\nconst SkeletonCard: React.FC<{\n  accentColor: string;\n  radius: number;\n  scale: number;\n  shimmer: number;\n}> = ({ accentColor, radius, scale, shimmer }) => (\n  <div\n    style={{\n      position: \"absolute\",\n      inset: 0,\n      background: \"rgba(16,16,24,0.88)\",\n      border: \"1px solid rgba(255,255,255,0.08)\",\n      borderRadius: radius,\n      backfaceVisibility: \"hidden\",\n      overflow: \"hidden\",\n    }}\n  >\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        background: `linear-gradient(110deg, transparent 0%, transparent ${shimmer}%, ${accentColor}24 ${shimmer + 16}%, transparent ${shimmer + 32}%)`,\n      }}\n    />\n    <div style={{ padding: 24 * scale }}>\n      <div\n        style={{\n          width: \"42%\",\n          height: 12 * scale,\n          background: \"rgba(255,255,255,0.09)\",\n          borderRadius: 999,\n        }}\n      />\n      <div\n        style={{\n          width: \"72%\",\n          height: 34 * scale,\n          background: \"rgba(255,255,255,0.1)\",\n          borderRadius: 10 * scale,\n          marginTop: 18 * scale,\n        }}\n      />\n      <div\n        style={{\n          width: \"100%\",\n          height: 82 * scale,\n          background: \"rgba(255,255,255,0.055)\",\n          borderRadius: 12 * scale,\n          marginTop: 20 * scale,\n        }}\n      />\n    </div>\n  </div>\n);\n\nconst MetricCard: React.FC<{\n  accentColor: string;\n  metric: AiGenerationMetric;\n  index: number;\n  progress: number;\n  radius: number;\n  scale: number;\n  width: number;\n}> = ({ accentColor, metric, index, progress, radius, scale, width }) => {\n  const variant = index % 3;\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        background:\n          \"linear-gradient(180deg, rgba(24,24,34,0.96), rgba(12,12,18,0.96))\",\n        border: `1px solid ${accentColor}38`,\n        borderRadius: radius,\n        backfaceVisibility: \"hidden\",\n        transform: \"rotateY(180deg)\",\n        overflow: \"hidden\",\n        boxShadow: `0 0 0 1px ${accentColor}14, 0 ${24 * scale}px ${70 * scale}px rgba(0,0,0,0.34)`,\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          width: 160 * scale,\n          height: 160 * scale,\n          right: -72 * scale,\n          top: -72 * scale,\n          borderRadius: 999,\n          background: `${accentColor}18`,\n          filter: `blur(${24 * scale}px)`,\n        }}\n      />\n      <div\n        style={{\n          position: \"relative\",\n          height: \"100%\",\n          padding: 24 * scale,\n          display: \"flex\",\n          flexDirection: \"column\",\n          justifyContent: \"space-between\",\n        }}\n      >\n        <div>\n          <div\n            style={{\n              fontSize: scaleFont(20, width),\n              color: \"rgba(255,255,255,0.58)\",\n              fontWeight: 600,\n              fontFamily,\n            }}\n          >\n            {metric.label}\n          </div>\n          <div\n            style={{\n              marginTop: 10 * scale,\n              fontSize: scaleFont(58, width),\n              lineHeight: 1,\n              fontWeight: 700,\n              color: \"white\",\n              fontFamily,\n            }}\n          >\n            {metric.value}\n          </div>\n          {metric.delta ? (\n            <div\n              style={{\n                marginTop: 10 * scale,\n                fontSize: scaleFont(19, width),\n                color: accentColor,\n                fontWeight: 700,\n              }}\n            >\n              {metric.delta} this week\n            </div>\n          ) : null}\n        </div>\n        {variant === 0 ? (\n          <MiniBarChart accentColor={accentColor} scale={scale} progress={progress} />\n        ) : variant === 1 ? (\n          <MiniRows scale={scale} progress={progress} />\n        ) : (\n          <div>\n            <MiniBarChart\n              accentColor={accentColor}\n              scale={scale}\n              progress={progress}\n            />\n            <MiniRows scale={scale} progress={progress} />\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport const AiGenerationCanvas: React.FC<AiGenerationCanvasProps> = ({\n  prompt = \"Generate a revenue dashboard for this launch\",\n  accentColor = \"#e8b86d\",\n  cardCount = 4,\n  metrics = DEFAULT_METRICS,\n  eyebrow = \"AI generation canvas\",\n  statusLabel = \"Generating\",\n  speed = 1,\n}) => {\n  const rawFrame = useCurrentFrame();\n  const frame = rawFrame * speed;\n  const { fps, width, height, durationInFrames } = useVideoConfig();\n  const safe = getSafeAreaPadding({ width, height });\n  const isPortrait = height > width;\n  const scale = width / 1920;\n  const safeWidth = width - safe.paddingLeft - safe.paddingRight;\n\n  /** Exit beat: the dashboard holds, then the whole canvas eases out before the last frame. */\n  const exitStart = durationInFrames - EXIT_DURATION;\n  const exitProgress = interpolate(\n    rawFrame,\n    [exitStart, durationInFrames - 1],\n    [0, 1],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\", easing: EASING.exit },\n  );\n\n  const promptEnd = 44;\n  const morphEnd = 78;\n  const skeletonEnd = 104;\n  const normalizedCardCount = clamp(Math.round(cardCount), 1, 6);\n  const visibleMetrics = Array.from({ length: normalizedCardCount }, (_, index) => {\n    return metrics[index % metrics.length] ?? DEFAULT_METRICS[index % DEFAULT_METRICS.length];\n  });\n\n  const typedCount = Math.floor(\n    interpolate(frame, [6, promptEnd - 4], [0, prompt.length], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: Easing.out(Easing.cubic),\n    }),\n  );\n  const typedText = prompt.slice(0, typedCount);\n  const cursorVisible = Math.floor(frame / 12) % 2 === 0;\n\n  const morph = spring({\n    frame: frame - promptEnd,\n    fps,\n    config: { mass: 1, damping: 17, stiffness: 118 },\n    durationInFrames: morphEnd - promptEnd,\n  });\n\n  const dashboardEnter = interpolate(frame, [morphEnd - 8, skeletonEnd], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  /** Settle the ambient glow and shimmer clocks before the exit fade begins,\n   *  so nothing is mid-cycle when the canvas dissolves. */\n  const settledRawFrame = Math.min(rawFrame, exitStart);\n  const settledFrame = Math.min(frame, exitStart);\n  const backgroundPulse = interpolate(\n    settledRawFrame % 120,\n    [0, 60, 120],\n    [0.74, 1, 0.74],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n  );\n  const shimmer = ((settledFrame * 1.35) % 180) - 38;\n\n  const promptSize = fittedPromptSize(prompt, width, isPortrait);\n  const headerPromptSize = scaleFont(isPortrait ? 24 : 18, width);\n  const inputHeight = interpolate(\n    morph,\n    [0, 1],\n    [scaleFont(isPortrait ? 132 : 106, width), scaleFont(isPortrait ? 76 : 62, width)],\n  );\n  const inputWidth = interpolate(\n    morph,\n    [0, 1],\n    [Math.min(safeWidth, scaleFont(isPortrait ? 860 : 760, width)), safeWidth],\n  );\n  const inputFontSize = interpolate(morph, [0, 1], [promptSize, headerPromptSize]);\n  const inputRadius = interpolate(\n    morph,\n    [0, 1],\n    [scaleFont(22, width), scaleFont(16, width)],\n  );\n  const inputTranslateY = interpolate(\n    morph,\n    [0, 1],\n    [isPortrait ? height * 0.27 : height * 0.3, 0],\n  );\n  const dashboardTranslate = interpolate(\n    dashboardEnter,\n    [0, 1],\n    [scaleFont(34, width), 0],\n  );\n\n  const columns = isPortrait\n    ? Math.min(2, normalizedCardCount)\n    : Math.min(normalizedCardCount, 4);\n  const radius = scaleFont(isPortrait ? 26 : 20, width);\n  const cardMinHeight = scaleFont(isPortrait ? 250 : 270, width);\n  const cardGap = scaleFont(isPortrait ? 22 : 24, width);\n\n  return (\n    <AbsoluteFill\n      style={{\n        background: DEMO_BG,\n        overflow: \"hidden\",\n        fontFamily,\n        opacity: 1 - exitProgress,\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          backgroundImage:\n            \"linear-gradient(rgba(255,255,255,0.032) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.032) 1px, transparent 1px)\",\n          backgroundSize: `${scaleFont(isPortrait ? 46 : 44, width)}px ${scaleFont(isPortrait ? 46 : 44, width)}px`,\n          maskImage:\n            \"radial-gradient(ellipse at center, black 36%, transparent 78%)\",\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          width: scaleFont(isPortrait ? 680 : 820, width),\n          height: scaleFont(isPortrait ? 680 : 820, width),\n          right: -scaleFont(isPortrait ? 250 : 180, width),\n          top: -scaleFont(isPortrait ? 150 : 220, width),\n          borderRadius: 999,\n          background: `${accentColor}24`,\n          filter: `blur(${scaleFont(90, width)}px)`,\n          opacity: backgroundPulse,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          width: scaleFont(isPortrait ? 520 : 620, width),\n          height: scaleFont(isPortrait ? 520 : 620, width),\n          left: -scaleFont(isPortrait ? 210 : 140, width),\n          bottom: -scaleFont(isPortrait ? 160 : 190, width),\n          borderRadius: 999,\n          background: \"rgba(45,212,191,0.16)\",\n          filter: `blur(${scaleFont(100, width)}px)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"relative\",\n          zIndex: 1,\n          height: \"100%\",\n          paddingTop: safe.paddingTop,\n          paddingRight: safe.paddingRight,\n          paddingBottom: safe.paddingBottom,\n          paddingLeft: safe.paddingLeft,\n          display: \"flex\",\n          flexDirection: \"column\",\n          gap: scaleFont(isPortrait ? 34 : 30, width),\n        }}\n      >\n        <div\n          style={{\n            minHeight: inputHeight,\n            display: \"flex\",\n            justifyContent: \"center\",\n            alignItems: \"flex-start\",\n          }}\n        >\n          <div\n            style={{\n              width: inputWidth,\n              height: inputHeight,\n              transform: `translateY(${inputTranslateY}px)`,\n              background: \"rgba(18,18,25,0.84)\",\n              border: `1px solid ${accentColor}66`,\n              borderRadius: inputRadius,\n              backdropFilter: \"blur(18px)\",\n              display: \"flex\",\n              alignItems: morph > 0.5 ? \"center\" : \"flex-start\",\n              padding: `0 ${scaleFont(isPortrait ? 24 : 22, width)}px`,\n              boxShadow:\n                morph < 0.5\n                  ? `0 0 0 ${scaleFont(8, width)}px ${accentColor}12, 0 ${scaleFont(34, width)}px ${scaleFont(100, width)}px rgba(0,0,0,0.52)`\n                  : `0 ${scaleFont(10, width)}px ${scaleFont(32, width)}px rgba(0,0,0,0.36)`,\n            }}\n          >\n            <div\n              style={{\n                width: scaleFont(isPortrait ? 36 : 28, width),\n                height: scaleFont(isPortrait ? 36 : 28, width),\n                marginTop: morph > 0.5 ? 0 : scaleFont(24, width),\n                marginRight: scaleFont(16, width),\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n                flexShrink: 0,\n              }}\n            >\n              <SparkIcon size={scaleFont(isPortrait ? 31 : 24, width)} color={accentColor} />\n            </div>\n            <div\n              style={{\n                minWidth: 0,\n                color: \"white\",\n                fontSize: inputFontSize,\n                lineHeight: 1.15,\n                fontWeight: 600,\n                paddingTop: morph > 0.5 ? 0 : scaleFont(22, width),\n                textOverflow: \"ellipsis\",\n                whiteSpace: morph > 0.7 ? \"nowrap\" : \"normal\",\n              }}\n            >\n              {typedText}\n              {frame < promptEnd && cursorVisible ? (\n                <span\n                  style={{\n                    display: \"inline-block\",\n                    width: Math.max(2, scaleFont(3, width)),\n                    height: inputFontSize,\n                    background: accentColor,\n                    marginLeft: scaleFont(4, width),\n                    verticalAlign: \"middle\",\n                  }}\n                />\n              ) : null}\n            </div>\n            <div\n              style={{\n                marginLeft: \"auto\",\n                display: \"flex\",\n                gap: scaleFont(10, width),\n                alignItems: \"center\",\n                opacity: interpolate(morph, [0.58, 1], [0, 1], {\n                  extrapolateLeft: \"clamp\",\n                  extrapolateRight: \"clamp\",\n                }),\n                flexShrink: 0,\n              }}\n            >\n              <div\n                style={{\n                  width: scaleFont(10, width),\n                  height: scaleFont(10, width),\n                  borderRadius: 999,\n                  background: accentColor,\n                  boxShadow: `0 0 ${scaleFont(18, width)}px ${accentColor}`,\n                }}\n              />\n              <div\n                style={{\n                  fontSize: scaleFont(isPortrait ? 20 : 16, width),\n                  color: \"rgba(255,255,255,0.66)\",\n                  fontWeight: 700,\n                }}\n              >\n                {statusLabel}\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <div\n          style={{\n            opacity: dashboardEnter,\n            transform: `translateY(${dashboardTranslate}px)`,\n            flex: 1,\n            minHeight: 0,\n            display: \"flex\",\n            flexDirection: \"column\",\n            gap: scaleFont(isPortrait ? 22 : 20, width),\n          }}\n        >\n          <div\n            style={{\n              display: \"flex\",\n              alignItems: isPortrait ? \"flex-start\" : \"flex-end\",\n              justifyContent: \"space-between\",\n              gap: scaleFont(24, width),\n              flexDirection: isPortrait ? \"column\" : \"row\",\n            }}\n          >\n            <div>\n              <div\n                style={{\n                  color: accentColor,\n                  fontSize: scaleFont(isPortrait ? 24 : 18, width),\n                  fontWeight: 700,\n                }}\n              >\n                {eyebrow}\n              </div>\n              <div\n                style={{\n                  marginTop: scaleFont(8, width),\n                  color: \"white\",\n                  fontSize: scaleFont(isPortrait ? 48 : 42, width),\n                  lineHeight: 1.04,\n                  fontWeight: 700,\n                  maxWidth: isPortrait ? safeWidth : safeWidth * 0.62,\n                }}\n              >\n                Live metrics from one prompt\n              </div>\n            </div>\n            <div\n              style={{\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: scaleFont(12, width),\n                color: \"rgba(255,255,255,0.68)\",\n                fontSize: scaleFont(isPortrait ? 22 : 18, width),\n                fontWeight: 600,\n              }}\n            >\n              <div\n                style={{\n                  width: scaleFont(9, width),\n                  height: scaleFont(9, width),\n                  borderRadius: 999,\n                  background: \"#2dd4bf\",\n                  boxShadow: `0 0 ${scaleFont(16, width)}px #2dd4bf`,\n                }}\n              />\n              Analysis ready\n            </div>\n          </div>\n\n          <div\n            style={{\n              flex: 1,\n              minHeight: 0,\n              display: \"grid\",\n              gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,\n              gridAutoRows: `minmax(${cardMinHeight}px, 1fr)`,\n              gap: cardGap,\n            }}\n          >\n            {visibleMetrics.map((metric, index) => {\n              const cardStart = skeletonEnd + index * 7;\n              const cardSpring = spring({\n                frame: frame - cardStart,\n                fps,\n                config: { mass: 0.9, damping: 15, stiffness: 112 },\n                durationInFrames: 28,\n              });\n              const rotation = interpolate(cardSpring, [0, 1], [0, 180]);\n              const cardProgress = interpolate(cardSpring, [0.45, 1], [0, 1], {\n                extrapolateLeft: \"clamp\",\n                extrapolateRight: \"clamp\",\n                easing: EASING.enter,\n              });\n\n              return (\n                <div\n                  key={`${metric.label}-${index}`}\n                  style={{\n                    position: \"relative\",\n                    perspective: scaleFont(1300, width),\n                    opacity: dashboardEnter,\n                    minHeight: cardMinHeight,\n                  }}\n                >\n                  <div\n                    style={{\n                      position: \"relative\",\n                      width: \"100%\",\n                      height: \"100%\",\n                      transformStyle: \"preserve-3d\",\n                      transform: `rotateY(${rotation}deg)`,\n                    }}\n                  >\n                    <SkeletonCard\n                      accentColor={accentColor}\n                      radius={radius}\n                      scale={scale}\n                      shimmer={shimmer}\n                    />\n                    <MetricCard\n                      accentColor={accentColor}\n                      metric={metric}\n                      index={index}\n                      progress={cardProgress}\n                      radius={radius}\n                      scale={scale}\n                      width={width}\n                    />\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "reels",
    "drive": "time",
    "tier": "advanced"
  }
}