---
name: social-clips
description: >
  Turn Slack threads into animated social videos with realistic UI details (avatars,
  typing indicators, reactions, spring animations) and render to MP4/GIF in vertical
  and horizontal formats.
tags: [video, slack, social, remotion]
version: 1.0.0
---

# Social Clips

Turn Slack threads into animated social videos. Slack dark mode with real profile photos, typing indicators, reactions, and spring animations.

Outputs: MP4 (vertical + horizontal) and GIF.

## Quick Start

```bash
npm install
npm run studio           # preview in browser
npm run render:stories   # 1080x1920 MP4
npm run render:landscape # 1920x1080 MP4
npm run gif:stories      # 1080x1920 GIF
npm run gif:landscape    # 1920x1080 GIF
```

Or render any composition directly:

```bash
npx remotion render <composition-id> out/<name>.mp4 --codec=h264 --crf=18
```

## Making a New Clip

### 1. Pull the Slack thread

```
mcp__slack__slack_get_thread_replies(channel_id, thread_ts)
```

Extract `thread_ts` from the URL: `p1234567890123456` â `1234567890.123456`

### 2. Get avatar photos

```
mcp__slack__slack_get_users(limit: 200)
```

Download `image_512` URLs into `src/assets/avatars/`:

```bash
curl -sL -o src/assets/avatars/name.jpg "https://avatars.slack-edge.com/..."
```

### 3. Add senders

In `src/slack-types.ts`:

1. Add to the `SlackSender` union type
2. Import the avatar image
3. Add a `SenderConfig` entry with `avatarPhoto`

The avatar component renders the photo when available, falls back to colored initials.

### 4. Write the data file

Create `src/data/<clip-name>.ts`:

```typescript
import type { SlackMessage, SlackTimedEvent } from '../slack-types';

export const MESSAGES: SlackMessage[] = [
  { id: 0, sender: 'dan', text: 'Opening message' },
  { id: 1, sender: 'r2c2', text: 'Reply with *bold* and @mentions' },
  { id: 2, sender: 'austin', text: 'Another message', reactions: [{ emoji: 'ð¥', count: 3 }] },
];

export const TIMELINE: SlackTimedEvent[] = [
  // Messages
  { type: 'message', messageIndex: 0, startFrame: 30, durationFrames: 40 },

  // Typing indicator before a reply
  { type: 'typing', typingSender: 'r2c2', startFrame: 75, durationFrames: 40 },
  { type: 'message', messageIndex: 1, startFrame: 115, durationFrames: 40 },

  // Human messages just appear (no typing indicator)
  { type: 'message', messageIndex: 2, startFrame: 165, durationFrames: 40 },

  // Reaction pops in after a message
  { type: 'reaction', messageIndex: 2, reactionIndex: 0, startFrame: 215, durationFrames: 20 },

  // Pause for tension
  { type: 'pause', typingSender: 'dan', startFrame: 240, durationFrames: 60 },
];

export const TOTAL_FRAMES = 1800; // 60s at 30fps
export const FPS = 30;
```

**Text supports:** `@mentions`, `*bold*`, `\n` newlines, `â¢` bullets

**Consecutive messages** from the same sender collapse the avatar + name automatically.

### 5. Register the composition

In `src/Root.tsx`:

```typescript
import { MESSAGES, TIMELINE, TOTAL_FRAMES, FPS } from './data/my-clip';

// Vertical
<Composition
  id="my-clip-stories"
  component={SlackScreen}
  durationInFrames={TOTAL_FRAMES}
  fps={FPS}
  width={1080}
  height={1920}
  defaultProps={{ messages: MESSAGES, timeline: TIMELINE, layout: 'portrait' }}
/>

// Horizontal
<Composition
  id="my-clip-landscape"
  component={SlackScreen}
  durationInFrames={TOTAL_FRAMES}
  fps={FPS}
  width={1920}
  height={1080}
  defaultProps={{ messages: MESSAGES, timeline: TIMELINE, layout: 'landscape' }}
/>
```

### 6. Render

```bash
npx remotion render my-clip-stories out/my-clip-stories.mp4 --codec=h264 --crf=18
npx remotion render my-clip-landscape out/my-clip-landscape.mp4 --codec=h264 --crf=18
```

GIF conversion:

```bash
# Vertical
ffmpeg -y -i out/my-clip-stories.mp4 \
  -vf "fps=15,scale=540:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3" \
  out/my-clip-stories.gif

# Horizontal
ffmpeg -y -i out/my-clip-landscape.mp4 \
  -vf "fps=15,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3" \
  out/my-clip-landscape.gif
```

## Narrative Arc

Find the spine of any thread:

| Beat | Msgs | Look for |
|------|------|----------|
| Hook | 1-2 | The inciting question |
| Brainstorm | 3-6 | Ideas flying, agents riffing |
| Conflict | 2-3 | Challenge, wrong turn, pushback |
| Breakthrough | 1-2 | The idea that lights everyone up |
| Eruption | 3-5 | Pile-on, excitement, reactions |
| Close | 1-2 | The line that crystallizes it |

Rules:
- ~80 words max per message
- 15-21 messages for 60-75s
- Agents get typing indicators, humans don't
- Put the longest pause before the breakthrough
- Eruption = fast pile-up (20-30 frame gaps)
- Final hold: 7+ seconds

## Timeline Reference

30fps. 30 frames = 1 second.

| Duration | Frames | Messages |
|----------|--------|----------|
| 60s | 1800 | 15-17 |
| 75s | 2250 | 18-21 |
| 90s | 2700 | 22-25 |

| Event | Frames | Notes |
|-------|--------|-------|
| Short message | 25-35 | ~1s read |
| Long message | 45-60 | ~2s read |
| Typing (fast) | 25-35 | Agent is quick |
| Typing (thinking) | 45-55 | Agent is deliberating |
| Brief pause | 20-40 | Beat |
| Big pause | 80-120 | Before breakthrough |
| Reaction | 20 | Quick pop |
| Final hold | 200-360 | Let it breathe |

## Components

| File | What |
|------|------|
| `SlackScreen` | Main composition â header, messages, typing, input bar |
| `SlackMessageRow` | Avatar, name, APP badge, text, reactions |
| `SlackAvatar` | Photo with colored-initial fallback |
| `SlackHeader` | "Thread" header with channel name (configurable) |
| `SlackTypingIndicator` | Animated dots with sender name |
| `SlackReactionPill` | Emoji + count pill |
| `SlackInputBar` | Input field chrome |

## Types

```typescript
type SlackSender = string;  // extend union in slack-types.ts

interface SlackMessage {
  id: number;
  text: string;
  sender: SlackSender;
  reactions?: Array<{ emoji: string; count: number }>;
}

interface SlackTimedEvent {
  type: 'message' | 'typing' | 'reaction' | 'pause';
  messageIndex?: number;
  reactionIndex?: number;
  typingSender?: SlackSender;
  startFrame: number;
  durationFrames: number;
}

interface SenderConfig {
  name: string;
  initials: string;
  avatarColor: string;
  isApp: boolean;
  avatarPhoto?: string;  // imported image path
}
```

## Existing Clips

| ID | Size | Content |
|----|------|---------|
| `plus-one-slack-stories` | 1080x1920 | Plus One naming (75s) |
| `plus-one-slack-landscape` | 1920x1080 | Plus One naming (75s) |
