diff --git a/rules/assets/charts-bar-chart.tsx b/rules/assets/charts-bar-chart.tsx
new file mode 100644
index 0000000..1313ebf
--- /dev/null
+++ b/rules/assets/charts-bar-chart.tsx
@@ -0,0 +1,173 @@
+import {loadFont} from '@remotion/google-fonts/Inter';
+import {AbsoluteFill, spring, useCurrentFrame, useVideoConfig} from 'remotion';
+
+const {fontFamily} = loadFont();
+
+const COLOR_BAR = '#D4AF37';
+const COLOR_TEXT = '#ffffff';
+const COLOR_MUTED = '#888888';
+const COLOR_BG = '#0a0a0a';
+const COLOR_AXIS = '#333333';
+
+// Ideal composition size: 1280x720
+
+const Title: React.FC<{children: React.ReactNode}> = ({children}) => (
+
+);
+
+const YAxis: React.FC<{steps: number[]; height: number}> = ({
+ steps,
+ height,
+}) => (
+
+ {steps
+ .slice()
+ .reverse()
+ .map((step) => (
+
+ {step.toLocaleString()}
+
+ ))}
+
+);
+
+const Bar: React.FC<{
+ height: number;
+ progress: number;
+}> = ({height, progress}) => (
+
+);
+
+const XAxis: React.FC<{
+ children: React.ReactNode;
+ labels: string[];
+ height: number;
+}> = ({children, labels, height}) => (
+
+
+ {children}
+
+
+ {labels.map((label) => (
+
+ {label}
+
+ ))}
+
+
+);
+
+export const MyAnimation = () => {
+ const frame = useCurrentFrame();
+ const {fps, height} = useVideoConfig();
+
+ const data = [
+ {month: 'Jan', price: 2039},
+ {month: 'Mar', price: 2160},
+ {month: 'May', price: 2327},
+ {month: 'Jul', price: 2426},
+ {month: 'Sep', price: 2634},
+ {month: 'Nov', price: 2672},
+ ];
+
+ const minPrice = 2000;
+ const maxPrice = 2800;
+ const priceRange = maxPrice - minPrice;
+ const chartHeight = height - 280;
+ const yAxisSteps = [2000, 2400, 2800];
+
+ return (
+
+ Gold Price 2024
+
+
+
+ d.month)}>
+ {data.map((item, i) => {
+ const progress = spring({
+ frame: frame - i * 5 - 10,
+ fps,
+ config: {damping: 18, stiffness: 80},
+ });
+
+ const barHeight =
+ ((item.price - minPrice) / priceRange) * chartHeight * progress;
+
+ return (
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/rules/assets/text-animations-typewriter.tsx b/rules/assets/text-animations-typewriter.tsx
new file mode 100644
index 0000000..89f62ea
--- /dev/null
+++ b/rules/assets/text-animations-typewriter.tsx
@@ -0,0 +1,100 @@
+import {
+ AbsoluteFill,
+ interpolate,
+ useCurrentFrame,
+ useVideoConfig,
+} from 'remotion';
+
+const COLOR_BG = '#ffffff';
+const COLOR_TEXT = '#000000';
+const FULL_TEXT = 'From prompt to motion graphics. This is Remotion.';
+const PAUSE_AFTER = 'From prompt to motion graphics.';
+const FONT_SIZE = 72;
+const FONT_WEIGHT = 700;
+const CHAR_FRAMES = 2;
+const CURSOR_BLINK_FRAMES = 16;
+const PAUSE_SECONDS = 1;
+
+// Ideal composition size: 1280x720
+
+const getTypedText = ({
+ frame,
+ fullText,
+ pauseAfter,
+ charFrames,
+ pauseFrames,
+}: {
+ frame: number;
+ fullText: string;
+ pauseAfter: string;
+ charFrames: number;
+ pauseFrames: number;
+}): string => {
+ const pauseIndex = fullText.indexOf(pauseAfter);
+ const preLen =
+ pauseIndex >= 0 ? pauseIndex + pauseAfter.length : fullText.length;
+
+ let typedChars = 0;
+ if (frame < preLen * charFrames) {
+ typedChars = Math.floor(frame / charFrames);
+ } else if (frame < preLen * charFrames + pauseFrames) {
+ typedChars = preLen;
+ } else {
+ const postPhase = frame - preLen * charFrames - pauseFrames;
+ typedChars = Math.min(
+ fullText.length,
+ preLen + Math.floor(postPhase / charFrames),
+ );
+ }
+ return fullText.slice(0, typedChars);
+};
+
+const Cursor: React.FC<{
+ frame: number;
+ blinkFrames: number;
+ symbol?: string;
+}> = ({frame, blinkFrames, symbol = '\u258C'}) => {
+ const opacity = interpolate(
+ frame % blinkFrames,
+ [0, blinkFrames / 2, blinkFrames],
+ [1, 0, 1],
+ {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'},
+ );
+
+ return {symbol};
+};
+
+export const MyAnimation = () => {
+ const frame = useCurrentFrame();
+ const {fps} = useVideoConfig();
+
+ const pauseFrames = Math.round(fps * PAUSE_SECONDS);
+
+ const typedText = getTypedText({
+ frame,
+ fullText: FULL_TEXT,
+ pauseAfter: PAUSE_AFTER,
+ charFrames: CHAR_FRAMES,
+ pauseFrames,
+ });
+
+ return (
+
+
+ {typedText}
+
+
+
+ );
+};
diff --git a/rules/assets/text-animations-word-highlight.tsx b/rules/assets/text-animations-word-highlight.tsx
new file mode 100644
index 0000000..e3929c5
--- /dev/null
+++ b/rules/assets/text-animations-word-highlight.tsx
@@ -0,0 +1,108 @@
+import {loadFont} from '@remotion/google-fonts/Inter';
+import React from 'react';
+import {
+ AbsoluteFill,
+ spring,
+ useCurrentFrame,
+ useVideoConfig,
+} from 'remotion';
+
+/*
+ * Highlight a word in a sentence with a spring-animated wipe effect.
+ */
+
+// Ideal composition size: 1280x720
+
+const COLOR_BG = '#ffffff';
+const COLOR_TEXT = '#000000';
+const COLOR_HIGHLIGHT = '#A7C7E7';
+const FULL_TEXT = 'This is Remotion.';
+const HIGHLIGHT_WORD = 'Remotion';
+const FONT_SIZE = 72;
+const FONT_WEIGHT = 700;
+const HIGHLIGHT_START_FRAME = 30;
+const HIGHLIGHT_WIPE_DURATION = 18;
+
+const {fontFamily} = loadFont();
+
+const Highlight: React.FC<{
+ word: string;
+ color: string;
+ delay: number;
+ durationInFrames: number;
+}> = ({word, color, delay, durationInFrames}) => {
+ const frame = useCurrentFrame();
+ const {fps} = useVideoConfig();
+
+ const highlightProgress = spring({
+ fps,
+ frame,
+ config: {damping: 200},
+ delay,
+ durationInFrames,
+ });
+ const scaleX = Math.max(0, Math.min(1, highlightProgress));
+
+ return (
+
+
+ {word}
+
+ );
+};
+
+export const MyAnimation = () => {
+ const highlightIndex = FULL_TEXT.indexOf(HIGHLIGHT_WORD);
+ const hasHighlight = highlightIndex >= 0;
+ const preText = hasHighlight ? FULL_TEXT.slice(0, highlightIndex) : FULL_TEXT;
+ const postText = hasHighlight
+ ? FULL_TEXT.slice(highlightIndex + HIGHLIGHT_WORD.length)
+ : '';
+
+ return (
+
+
+ {hasHighlight ? (
+ <>
+ {preText}
+
+ {postText}
+ >
+ ) : (
+ {FULL_TEXT}
+ )}
+
+
+ );
+};