Messages
Appointment Reminder
A reminder with buttons: the customer taps Confirm and the confirmation comes back.
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
export type AppointmentReminderProps = {
/** Business name in the chat header. */
business?: string;
/** Customer first name. */
name?: string;
/** When the appointment is, e.g. "tomorrow". */
day?: string;
/** Time of the appointment. */
time?: string;
/** Color of the header and the button. */
accent?: string;
/** Replay forever with a pause between runs. */
loop?: boolean;
className?: string;
};
export default function AppointmentReminder({
business = "Acme",
name = "Sam",
day = "tomorrow",
time = "9:30 AM",
accent = "#1f5bff",
loop = true,
className = "",
}: AppointmentReminderProps) {
const root = useRef<HTMLDivElement>(null);
useGSAP(
() => {
const q = gsap.utils.selector(root);
const screen = q("[data-screen]")[0] as HTMLElement;
const confirm = q("[data-confirm]")[0] as HTMLElement;
const cursor = q("[data-cursor]")[0];
const m1 = q("[data-m1]")[0];
const acts = q("[data-acts]")[0];
const m2 = q("[data-m2]")[0];
const m3 = q("[data-m3]")[0];
// Centro do botão Confirmar, medido na tela (o cursor tem 22px). k desfaz a escala de quem
// mostra o componente reduzido: o retângulo medido vem escalado, o left/top do cursor não.
const aim = () => {
const a = screen.getBoundingClientRect();
const b = confirm.getBoundingClientRect();
const k = a.width ? screen.offsetWidth / a.width : 1;
return { x: (b.left - a.left + b.width / 2) * k - 11, y: (b.top - a.top + b.height / 2) * k - 11 };
};
const tl = gsap.timeline({ repeat: loop ? -1 : 0, repeatDelay: 1.4 });
tl.set([m1, acts, m2, m3], { autoAlpha: 0, y: 12 })
.set([m2, m3], { display: "none" })
.set(acts, { clearProps: "height" })
.set(cursor, { autoAlpha: 0, left: 180, top: 470 })
.set(confirm, { backgroundColor: "#ffffff" })
.to(m1, { autoAlpha: 1, y: 0, duration: 0.4, ease: "back.out(1.5)" }, 0.4)
.to(acts, { autoAlpha: 1, y: 0, duration: 0.35 }, 0.8)
.to(cursor, { autoAlpha: 1, duration: 0.2 }, 1.7)
.to(cursor, { left: () => aim().x, top: () => aim().y, duration: 0.6, ease: "power2.inOut" }, 1.7)
.to(cursor, { scale: 0.75, duration: 0.1, yoyo: true, repeat: 1 }, 2.35)
.to(confirm, { backgroundColor: "#e3eefa", duration: 0.15 }, 2.4)
.to(cursor, { autoAlpha: 0, duration: 0.25 }, 2.7)
.to(acts, { autoAlpha: 0, height: 0, duration: 0.3 }, 2.8)
.set(m2, { display: "block" }, 2.8)
.to(m2, { autoAlpha: 1, y: 0, duration: 0.3 }, 2.8)
.set(m3, { display: "block" }, 3.6)
.to(m3, { autoAlpha: 1, y: 0, duration: 0.4, ease: "back.out(1.5)" }, 3.6)
.to({}, { duration: 1.6 });
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) tl.progress(1).pause();
},
{ scope: root, dependencies: [loop, name, day, time] },
);
const initials = business
.split(" ")
.map((w) => w[0])
.join("")
.slice(0, 2)
.toUpperCase();
return (
<div
ref={root}
className={`relative h-[520px] w-[290px] shrink-0 overflow-hidden rounded-[34px] bg-neutral-950 p-[9px] shadow-2xl ${className}`}
>
<div data-screen className="relative h-full w-full overflow-hidden rounded-[26px] bg-[#efe7dd] text-[#111b16]">
<div className="flex h-[54px] items-center gap-2.5 px-3 text-white" style={{ backgroundColor: accent }}>
<div
className="grid h-8 w-8 place-items-center rounded-full bg-white text-[13px] font-bold"
style={{ color: accent }}
>
{initials}
</div>
<div>
<b className="block text-[13.5px]">{business}</b>
<small className="text-[11px] opacity-85">business account</small>
</div>
</div>
<div className="absolute inset-x-2 bottom-[60px] flex flex-col gap-1">
<div
data-m1
className="relative max-w-[84%] self-start rounded-lg bg-white px-2 pt-1.5 pb-3.5 text-[13px] leading-snug"
>
Hi {name}! Reminder: your appointment is {day} at {time}.
<span className="absolute right-1.5 bottom-0.5 text-[10px] text-[#56645c]">9:00</span>
</div>
<div data-acts className="flex w-[84%] flex-col gap-1 self-start overflow-hidden">
<span
data-confirm
className="rounded-lg bg-white py-2 text-center text-[13px] font-semibold shadow-sm"
style={{ color: accent }}
>
Confirm
</span>
<span
className="rounded-lg bg-white py-2 text-center text-[13px] font-semibold shadow-sm"
style={{ color: accent }}
>
Reschedule
</span>
</div>
<div
data-m2
className="relative max-w-[84%] self-end rounded-lg bg-[#d7f7c9] px-2 pt-1.5 pb-3.5 text-[13px]"
>
Confirm
<span className="absolute right-1.5 bottom-0.5 text-[10px] text-[#56645c]">9:04 ✓✓</span>
</div>
<div
data-m3
className="relative max-w-[84%] self-start rounded-lg bg-white px-2 pt-1.5 pb-3.5 text-[13px] leading-snug"
>
Confirmed! See you {day} at {time}. Please arrive 10 minutes early.
<span className="absolute right-1.5 bottom-0.5 text-[10px] text-[#56645c]">9:04</span>
</div>
</div>
<div className="absolute inset-x-2 bottom-2 flex h-[42px] items-center rounded-3xl bg-white px-3.5 text-[13px] text-[#69736d]">
Message
</div>
<div
data-cursor
aria-hidden="true"
className="absolute z-10 h-[22px] w-[22px] rounded-full border-2 border-[#1a1916] bg-[#1a1916]/25"
/>
</div>
</div>
);
}
Paste this into Claude Code inside your project. It installs the skills your agent needs, asks for your texts and colors, and recreates this animation in your stack.
SkillsGSAP skillsHyperFrames · optional, for video
Claude Code
Recreate the "Appointment Reminder" animated component from Motionshelf (https://motionshelf-pi.vercel.app) in my current project, adapted to my brand. Answer me in the language I write to you in. Work in this order: 1. Tools: Check whether the official GSAP skills are installed (gsap-core, gsap-timeline, gsap-react). If they are not, ask me and then run `npx skills add https://github.com/greensock/gsap-skills` (choose Claude Code). If they do not show up in this session yet, read the SKILL.md files the installer created for gsap-timeline and gsap-react before writing code. 2. Project: Look at my project: framework (Next.js, Vite, plain HTML…), TypeScript or JavaScript, Tailwind or plain CSS. Build it as one component that fits this stack. In React use gsap and @gsap/react; in plain HTML use GSAP from npm or a CDN. 3. Ask me before coding: Ask me these in a single message, showing the example values as defaults: - the business name and the customer's first name - the day and time of the appointment, and the confirmation text - the header and button color - the language of the texts 4. The animation What it looks like: A phone with a business chat: a reminder message with two buttons (Confirm and Reschedule) under it. Step by step: - At 0.4 s the reminder bubble pops in (back.out); at 0.8 s the two buttons appear under it. - At 1.7 s a round cursor appears and moves to the Confirm button (0.6 s), presses it, and the button tints. - The buttons collapse (height to 0), the customer's "Confirm" bubble appears on the right, and at 3.6 s the business replies with the confirmation. Rules: - Use one GSAP timeline that loops forever with a short pause between runs (repeat: -1). If the step by step gives the pause, use that value; otherwise repeatDelay about 1.4 s. - Set every starting state with set() at the start of the timeline, never with from(): on the second loop from() starts from the wrong place. - Elements that appear later and would push the layout (new chat bubbles, new rows) stay display:none until their turn; elements that already have their place in the layout (a subtitle, a button) start hidden with autoAlpha 0, so nothing jumps. - If there is a cursor or pointer, it aims at the measured center of its target (getBoundingClientRect), never at fixed pixel coordinates; if the component is scaled, divide by the scale. - Blur only in the middle of a movement and always end at blur(0). - With prefers-reduced-motion, jump to the final frame and stop (tl.progress(1).pause()). - In React, use useGSAP from @gsap/react with a scope ref, so the timeline is cleaned up on unmount. - All example content stays generic (no real brands, logos or people). - The bubbles that appear later use display:none until their turn, and the button area gets its height back at the start of each loop (clearProps). 5. Check: Run it and watch the first and the third loop (they must look the same). Check it at 390 px wide with no sideways scroll, with reduced motion turned on, and with a clean console. Tell me what you checked. 6. Video (optional): If I also want this as an MP4 (ads, stories, WhatsApp status), install the HyperFrames plugin with `claude plugin marketplace add heygen-com/hyperframes` and `claude plugin install hyperframes@hyperframes`, then use /hyperframes:hyperframes to render the same animation in 9:16, 1:1 or 16:9. Rendering needs FFmpeg installed, or render with --docker (downloads about 3.3 GB the first time). Ask me before installing. Shortcut: If I only want the ready-made component instead, it installs with `npx shadcn@latest add https://motionshelf-pi.vercel.app/r/appointment-reminder.json`.
Other agentsUsing Cursor, Codex, Gemini CLI or another agent? This version tells it to find its own way to load the same skills before it starts.
Other agents
Recreate the "Appointment Reminder" animated component from Motionshelf (https://motionshelf-pi.vercel.app) in my current project, adapted to my brand. Answer me in the language I write to you in. Work in this order: 1. Tools: These instructions were written for Claude Code, which uses "skills" (folders with a SKILL.md that teach it a technique). First find out how you load skills, rules or instructions, and get the same knowledge: (a) if your agent is supported by the skills installer, ask me and run `npx skills add https://github.com/greensock/gsap-skills --agent <your-agent>` (for example codex, opencode, antigravity); (b) otherwise copy the skill folders from https://github.com/greensock/gsap-skills into your skills or rules directory (in Cursor: Settings → Rules → Remote Rule (GitHub) → greensock/gsap-skills); (c) if you cannot load skills at all, open the SKILL.md files in https://github.com/greensock/gsap-skills and read them as instructions before coding. Tell me which path you used. 2. Project: Look at my project: framework (Next.js, Vite, plain HTML…), TypeScript or JavaScript, Tailwind or plain CSS. Build it as one component that fits this stack. In React use gsap and @gsap/react; in plain HTML use GSAP from npm or a CDN. 3. Ask me before coding: Ask me these in a single message, showing the example values as defaults: - the business name and the customer's first name - the day and time of the appointment, and the confirmation text - the header and button color - the language of the texts 4. The animation What it looks like: A phone with a business chat: a reminder message with two buttons (Confirm and Reschedule) under it. Step by step: - At 0.4 s the reminder bubble pops in (back.out); at 0.8 s the two buttons appear under it. - At 1.7 s a round cursor appears and moves to the Confirm button (0.6 s), presses it, and the button tints. - The buttons collapse (height to 0), the customer's "Confirm" bubble appears on the right, and at 3.6 s the business replies with the confirmation. Rules: - Use one GSAP timeline that loops forever with a short pause between runs (repeat: -1). If the step by step gives the pause, use that value; otherwise repeatDelay about 1.4 s. - Set every starting state with set() at the start of the timeline, never with from(): on the second loop from() starts from the wrong place. - Elements that appear later and would push the layout (new chat bubbles, new rows) stay display:none until their turn; elements that already have their place in the layout (a subtitle, a button) start hidden with autoAlpha 0, so nothing jumps. - If there is a cursor or pointer, it aims at the measured center of its target (getBoundingClientRect), never at fixed pixel coordinates; if the component is scaled, divide by the scale. - Blur only in the middle of a movement and always end at blur(0). - With prefers-reduced-motion, jump to the final frame and stop (tl.progress(1).pause()). - In React, use useGSAP from @gsap/react with a scope ref, so the timeline is cleaned up on unmount. - All example content stays generic (no real brands, logos or people). - The bubbles that appear later use display:none until their turn, and the button area gets its height back at the start of each loop (clearProps). 5. Check: Run it and watch the first and the third loop (they must look the same). Check it at 390 px wide with no sideways scroll, with reduced motion turned on, and with a clean console. Tell me what you checked. 6. Video (optional): If I also want this as an MP4 (ads, stories, WhatsApp status), install the HyperFrames skills with `npx skills add heygen-com/hyperframes` (choose your agent) and render the same animation in 9:16, 1:1 or 16:9. Rendering needs FFmpeg installed, or render with --docker (downloads about 3.3 GB the first time). Ask me before installing. Shortcut: If I only want the ready-made component instead, it installs with `npx shadcn@latest add https://motionshelf-pi.vercel.app/r/appointment-reminder.json`.
Install
npx shadcn@latest add https://motionshelf-pi.vercel.app/r/appointment-reminder.jsonCopies the file into your project. Requires React 19, Tailwind CSS v4 and GSAP (installed as a dependency).
Props
| Name | Type | Default | Description |
|---|---|---|---|
| business | string | "Acme" | Business name in the chat header. |
| name | string | "Sam" | Customer first name. |
| day | string | "tomorrow" | When the appointment is. |
| time | string | "9:30 AM" | Time of the appointment. |
| accent | string | "#1f5bff" | Color of the accent. |
| loop | boolean | true | Replay forever with a pause between runs. |
| className | string | "" | Extra classes for the outer element. |
Dependencies
- gsap
- @gsap/react