{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "telegram-chat-flow",
  "title": "UI Telegram Chat Flow",
  "description": "A Telegram-styled messaging composition: tailed bubbles (white incoming, blue outgoing) with in-bubble timestamps and delivery checks, a header that flips to 'typing…' while the other side replies, reaction chips, and a Telegram composer that types the outgoing message before sending. Self-contained — it owns its timeline and only composes the caret primitive.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/remocn-ui", "@remocn/caret"],
  "files": [
    {
      "path": "registry/remocn-ui/telegram-chat-flow/index.tsx",
      "content": "\"use client\";\n\nimport { interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { Caret } from \"@/components/remocn/caret\";\nimport { type RemocnTheme, revealedText } from \"@/lib/remocn-ui\";\n\nexport interface TelegramMessage {\n  from: \"me\" | \"them\";\n  text: string;\n  reaction?: string;\n  time?: string;\n}\n\nexport interface TelegramContact {\n  name: string;\n  avatar?: string;\n}\n\nexport interface TelegramChatFlowProps {\n  messages?: TelegramMessage[];\n  contact?: TelegramContact;\n  accentColor?: string;\n  speed?: number;\n  theme?: Partial<RemocnTheme>;\n}\n\nconst MOBILE_WIDTH = 460;\nconst LEAD_IN = 12;\nconst FRAMES_PER_CHAR = 2.2;\nconst MIN_TYPE = 18;\nconst MAX_TYPE = 86;\nconst SEND_GAP = 10;\nconst REVEAL = 14;\nconst REACT_DELAY = 8;\nconst REACT_DUR = 14;\nconst MSG_GAP = 16;\nconst TYPING_MIN = 34;\nconst TYPING_MAX = 70;\nconst TAIL = 28;\nconst PRESS_WINDOW = 7;\n\nconst TELEGRAM_BLUE = \"#3390ec\";\nconst INCOMING_BG = \"#ffffff\";\nconst INCOMING_FG = \"#0f1419\";\nconst INCOMING_META = \"#8a99a5\";\nconst OUTGOING_FG = \"#ffffff\";\nconst OUTGOING_META = \"rgba(255,255,255,0.82)\";\n\nconst DEFAULT_MESSAGES: TelegramMessage[] = [\n  { from: \"me\", text: \"Hey — ready for the demo?\", time: \"9:40\" },\n  {\n    from: \"them\",\n    text: \"Yep, pushing it live now\",\n    reaction: \"🔥\",\n    time: \"9:41\",\n  },\n  {\n    from: \"me\",\n    text: \"Perfect, sending the link over\",\n    reaction: \"👍\",\n    time: \"9:41\",\n  },\n];\n\nfunction clamp(value: number, lo: number, hi: number): number {\n  return Math.max(lo, Math.min(hi, value));\n}\n\nexport interface ScheduledMessage {\n  index: number;\n  from: \"me\" | \"them\";\n  text: string;\n  reaction?: string;\n  time?: string;\n  presenceStart: number;\n  typeStart?: number;\n  sendAt?: number;\n  typingStart?: number;\n  revealAt: number;\n  reactAt?: number;\n}\n\nexport interface TelegramChatFlowSchedule {\n  items: ScheduledMessage[];\n  duration: number;\n}\n\nexport function telegramChatFlowSchedule(\n  messages: TelegramMessage[],\n): TelegramChatFlowSchedule {\n  const items: ScheduledMessage[] = [];\n  let cursor = LEAD_IN;\n\n  messages.forEach((message, index) => {\n    const hasReaction =\n      message.reaction !== undefined && message.reaction !== \"\";\n    if (message.from === \"me\") {\n      const typeStart = cursor;\n      const typeDur = clamp(\n        Math.round(message.text.length * FRAMES_PER_CHAR),\n        MIN_TYPE,\n        MAX_TYPE,\n      );\n      const sendAt = typeStart + typeDur + SEND_GAP;\n      const revealAt = sendAt;\n      const reactAt = hasReaction ? revealAt + REVEAL + REACT_DELAY : undefined;\n      items.push({\n        index,\n        from: \"me\",\n        text: message.text,\n        reaction: hasReaction ? message.reaction : undefined,\n        time: message.time,\n        presenceStart: sendAt - 2,\n        typeStart,\n        sendAt,\n        revealAt,\n        reactAt,\n      });\n      cursor =\n        revealAt +\n        REVEAL +\n        (hasReaction ? REACT_DELAY + REACT_DUR : 0) +\n        MSG_GAP;\n    } else {\n      const typingStart = cursor;\n      const typingDur = clamp(\n        Math.round(message.text.length * FRAMES_PER_CHAR),\n        TYPING_MIN,\n        TYPING_MAX,\n      );\n      const revealAt = typingStart + typingDur;\n      const reactAt = hasReaction ? revealAt + REVEAL + REACT_DELAY : undefined;\n      items.push({\n        index,\n        from: \"them\",\n        text: message.text,\n        reaction: hasReaction ? message.reaction : undefined,\n        time: message.time,\n        presenceStart: revealAt,\n        typingStart,\n        revealAt,\n        reactAt,\n      });\n      cursor =\n        revealAt +\n        REVEAL +\n        (hasReaction ? REACT_DELAY + REACT_DUR : 0) +\n        MSG_GAP;\n    }\n  });\n\n  const duration = Math.max(cursor - MSG_GAP + TAIL, LEAD_IN + TAIL);\n  return { items, duration };\n}\n\nexport function telegramChatFlowDuration(\n  messages: TelegramMessage[] = DEFAULT_MESSAGES,\n  speed = 1,\n): number {\n  const raw = telegramChatFlowSchedule(messages).duration;\n  return Math.ceil(raw / (speed <= 0 ? 1 : speed));\n}\n\nexport function sendPulse(items: ScheduledMessage[], eff: number): number {\n  let best = 0;\n  for (const item of items) {\n    if (item.sendAt === undefined) continue;\n    const distance = Math.abs(eff - item.sendAt);\n    if (distance <= PRESS_WINDOW) {\n      best = Math.max(best, 1 - distance / PRESS_WINDOW);\n    }\n  }\n  return best;\n}\n\nfunction Avatar({ contact, size }: { contact: TelegramContact; size: number }) {\n  const initial = contact.name.trim().charAt(0).toUpperCase();\n  return (\n    <div\n      style={{\n        flexShrink: 0,\n        width: size,\n        height: size,\n        minWidth: size,\n        borderRadius: \"50%\",\n        overflow: \"hidden\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        background: `linear-gradient(180deg, #72d5fd 0%, ${TELEGRAM_BLUE} 100%)`,\n        color: \"#ffffff\",\n        fontSize: size * 0.42,\n        fontWeight: 600,\n        fontFamily:\n          \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\",\n      }}\n    >\n      {contact.avatar !== undefined ? (\n        // biome-ignore lint/performance/noImgElement: Remotion output, not a Next.js app — next/image isn't available where this component ships\n        <img\n          src={contact.avatar}\n          alt={contact.name}\n          style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n        />\n      ) : (\n        initial\n      )}\n    </div>\n  );\n}\n\nfunction DoubleCheck({ color }: { color: string }) {\n  return (\n    <svg\n      width={16}\n      height={16}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"M18 6 7 17l-5-5\" />\n      <path d=\"m22 10-7.5 7.5L13 16\" />\n    </svg>\n  );\n}\n\nfunction PaperPlane({ color }: { color: string }) {\n  return (\n    <svg\n      width={20}\n      height={20}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\" />\n      <path d=\"m21.854 2.147-10.94 10.939\" />\n    </svg>\n  );\n}\n\nfunction MicIcon({ color }: { color: string }) {\n  return (\n    <svg\n      width={22}\n      height={22}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"M12 19v3\" />\n      <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n      <rect x={9} y={2} width={6} height={13} rx={3} />\n    </svg>\n  );\n}\n\nfunction SmileIcon({ color }: { color: string }) {\n  return (\n    <svg\n      width={22}\n      height={22}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <circle cx={12} cy={12} r={10} />\n      <path d=\"M8 14s1.5 2 4 2 4-2 4-2\" />\n      <line x1={9} x2={9.01} y1={9} y2={9} />\n      <line x1={15} x2={15.01} y1={9} y2={9} />\n    </svg>\n  );\n}\n\nfunction AttachIcon({ color }: { color: string }) {\n  return (\n    <svg\n      width={22}\n      height={22}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551\" />\n    </svg>\n  );\n}\n\nfunction PhoneIcon({ color }: { color: string }) {\n  return (\n    <svg\n      width={22}\n      height={22}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384\" />\n    </svg>\n  );\n}\n\nfunction MoreIcon({ color }: { color: string }) {\n  return (\n    <svg\n      width={22}\n      height={22}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <circle cx={12} cy={12} r={1} />\n      <circle cx={12} cy={5} r={1} />\n      <circle cx={12} cy={19} r={1} />\n    </svg>\n  );\n}\n\nfunction BubbleTail({\n  side,\n  color,\n}: {\n  side: \"left\" | \"right\";\n  color: string;\n}) {\n  const path =\n    side === \"right\"\n      ? \"M0 0 H4 C4 7 7 12 13 13 C6 13 0 9 0 0 Z\"\n      : \"M13 0 H9 C9 7 6 12 0 13 C7 13 13 9 13 0 Z\";\n  return (\n    <svg\n      width={13}\n      height={13}\n      viewBox=\"0 0 13 13\"\n      style={{\n        position: \"absolute\",\n        bottom: 0,\n        left: side === \"left\" ? -6 : undefined,\n        right: side === \"right\" ? -6 : undefined,\n      }}\n    >\n      <path d={path} fill={color} />\n    </svg>\n  );\n}\n\nexport function TelegramChatFlow({\n  messages = DEFAULT_MESSAGES,\n  contact,\n  accentColor,\n  speed = 1,\n}: TelegramChatFlowProps) {\n  const frame = useCurrentFrame();\n  const eff = frame * speed;\n  const accent = accentColor ?? TELEGRAM_BLUE;\n\n  const { items } = telegramChatFlowSchedule(messages);\n\n  const activeMe = items.find(\n    (item) =>\n      item.from === \"me\" &&\n      item.typeStart !== undefined &&\n      item.sendAt !== undefined &&\n      eff >= item.typeStart &&\n      eff < item.sendAt,\n  );\n  let composerText = \"\";\n  let typing = false;\n  if (\n    activeMe &&\n    activeMe.typeStart !== undefined &&\n    activeMe.sendAt !== undefined\n  ) {\n    const typeDur = Math.max(\n      activeMe.sendAt - SEND_GAP - activeMe.typeStart,\n      1,\n    );\n    const progress = clamp((eff - activeMe.typeStart) / typeDur, 0, 1);\n    composerText = revealedText(\n      activeMe.text,\n      Math.floor(progress * activeMe.text.length),\n    );\n    typing = true;\n  }\n\n  const themTyping = items.some(\n    (item) =>\n      item.from === \"them\" &&\n      item.typingStart !== undefined &&\n      eff >= item.typingStart &&\n      eff < item.revealAt,\n  );\n\n  const sendActive = composerText.length > 0;\n  const sendScale = 1 - 0.16 * sendPulse(items, eff);\n  const present = items.filter((item) => eff >= item.presenceStart);\n\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        width: \"100%\",\n        height: \"100%\",\n        display: \"flex\",\n        justifyContent: \"center\",\n        background: \"transparent\",\n        fontFamily:\n          \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\",\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          width: \"100%\",\n          maxWidth: MOBILE_WIDTH,\n          height: \"100%\",\n          boxSizing: \"border-box\",\n        }}\n      >\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 12,\n            padding: \"12px 14px\",\n            background: \"#ffffff\",\n            boxShadow: \"0 1px 0 rgba(0,0,0,0.06)\",\n          }}\n        >\n          {contact !== undefined && <Avatar contact={contact} size={38} />}\n          <div\n            style={{\n              display: \"flex\",\n              flexDirection: \"column\",\n              gap: 0,\n              flex: 1,\n            }}\n          >\n            <span\n              style={{\n                fontSize: 15,\n                fontWeight: 600,\n                lineHeight: 1.2,\n                letterSpacing: \"-0.01em\",\n                color: \"#0f1419\",\n              }}\n            >\n              {contact?.name ?? \"Chat\"}\n            </span>\n            <span\n              style={{\n                fontSize: 13,\n                lineHeight: 1.15,\n                color: accent,\n                fontStyle: themTyping ? \"italic\" : \"normal\",\n              }}\n            >\n              {themTyping ? \"typing…\" : \"online\"}\n            </span>\n          </div>\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: 18 }}>\n            <PhoneIcon color=\"#8a99a5\" />\n            <MoreIcon color=\"#8a99a5\" />\n          </div>\n        </div>\n\n        <div\n          style={{\n            position: \"relative\",\n            flex: 1,\n            minHeight: 0,\n            overflow: \"hidden\",\n          }}\n        >\n          <div\n            style={{\n              display: \"flex\",\n              flexDirection: \"column\",\n              justifyContent: \"flex-end\",\n              gap: 4,\n              minHeight: \"100%\",\n              padding: \"16px 12px 14px\",\n            }}\n          >\n            <div\n              style={{\n                display: \"flex\",\n                justifyContent: \"center\",\n                marginBottom: 8,\n              }}\n            >\n              <span\n                style={{\n                  padding: \"3px 11px\",\n                  borderRadius: 14,\n                  background: \"rgba(0,0,0,0.18)\",\n                  color: \"#ffffff\",\n                  fontSize: 12,\n                  fontWeight: 500,\n                }}\n              >\n                Today\n              </span>\n            </div>\n            {present.map((item) => (\n              <TelegramRow\n                key={item.index}\n                item={item}\n                eff={eff}\n                contact={contact}\n                accent={accent}\n              />\n            ))}\n          </div>\n        </div>\n\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"flex-end\",\n            gap: 8,\n            padding: \"8px 10px 12px\",\n            background: \"#ffffff\",\n            boxShadow: \"0 -1px 0 rgba(0,0,0,0.06)\",\n          }}\n        >\n          <div\n            style={{\n              flex: 1,\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: 8,\n              minHeight: 44,\n              padding: \"0 12px\",\n              borderRadius: 22,\n              background: \"#f1f3f5\",\n            }}\n          >\n            <SmileIcon color=\"#8a99a5\" />\n            <div\n              style={{\n                flex: 1,\n                display: \"flex\",\n                alignItems: \"center\",\n                fontSize: 15,\n                color: sendActive ? \"#0f1419\" : \"#8a99a5\",\n              }}\n            >\n              <span style={{ whiteSpace: \"pre-wrap\", wordBreak: \"break-word\" }}>\n                {sendActive ? composerText : \"Message\"}\n              </span>\n              {typing && (\n                <Caret\n                  color={accent}\n                  height={18}\n                  radius={1}\n                  blink\n                  marginLeft={composerText.length > 0 ? 2 : 0}\n                />\n              )}\n            </div>\n            <AttachIcon color=\"#8a99a5\" />\n          </div>\n          <div\n            style={{\n              flexShrink: 0,\n              width: 46,\n              height: 46,\n              borderRadius: \"50%\",\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              background: accent,\n              transform: `scale(${sendScale})`,\n            }}\n          >\n            {sendActive ? (\n              <PaperPlane color=\"#ffffff\" />\n            ) : (\n              <MicIcon color=\"#ffffff\" />\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction TelegramRow({\n  item,\n  eff,\n  contact,\n  accent,\n}: {\n  item: ScheduledMessage;\n  eff: number;\n  contact?: TelegramContact;\n  accent: string;\n}) {\n  const { fps } = useVideoConfig();\n  const outgoing = item.from === \"me\";\n\n  const enter = spring({\n    fps,\n    frame: eff - item.revealAt,\n    config: { damping: 16, stiffness: 200, mass: 0.7 },\n  });\n  const opacity = interpolate(enter, [0, 1], [0, 1]);\n  const translateY = interpolate(enter, [0, 1], [10, 0]);\n  const scale = interpolate(enter, [0, 1], [0.92, 1]);\n\n  let reactionScale = 0;\n  let reactionOpacity = 0;\n  if (item.reactAt !== undefined) {\n    reactionScale = spring({\n      fps,\n      frame: eff - item.reactAt,\n      config: { damping: 11, stiffness: 220, mass: 0.6 },\n    });\n    reactionOpacity = interpolate(\n      eff,\n      [item.reactAt, item.reactAt + 5],\n      [0, 1],\n      {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n      },\n    );\n  }\n\n  const bg = outgoing ? accent : INCOMING_BG;\n  const fg = outgoing ? OUTGOING_FG : INCOMING_FG;\n  const metaColor = outgoing ? OUTGOING_META : INCOMING_META;\n  const tailColor = outgoing ? accent : INCOMING_BG;\n\n  const bubble = (\n    <div\n      style={{\n        position: \"relative\",\n        maxWidth: \"76%\",\n        padding: \"6px 11px 7px\",\n        background: bg,\n        color: fg,\n        borderRadius: 16,\n        borderBottomRightRadius: outgoing ? 6 : 16,\n        borderBottomLeftRadius: outgoing ? 16 : 6,\n        boxShadow: outgoing ? \"none\" : \"0 1px 1px rgba(0,0,0,0.1)\",\n        fontSize: 15,\n        lineHeight: 1.35,\n      }}\n    >\n      <span style={{ wordBreak: \"break-word\", overflowWrap: \"break-word\" }}>\n        {item.text}\n      </span>\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent:\n            item.reaction !== undefined ? \"space-between\" : \"flex-end\",\n          gap: 8,\n          marginTop: 2,\n        }}\n      >\n        {item.reaction !== undefined && (\n          <span\n            style={{\n              display: \"inline-flex\",\n              alignItems: \"center\",\n              gap: 4,\n              padding: \"1px 7px\",\n              borderRadius: 11,\n              background: outgoing\n                ? \"rgba(255,255,255,0.22)\"\n                : \"rgba(51,144,236,0.12)\",\n              color: outgoing ? \"#ffffff\" : accent,\n              fontSize: 13,\n              fontWeight: 600,\n              opacity: reactionOpacity,\n              transform: `scale(${reactionScale})`,\n              transformOrigin: \"left center\",\n            }}\n          >\n            {item.reaction} 1\n          </span>\n        )}\n        <span\n          style={{\n            display: \"inline-flex\",\n            alignItems: \"center\",\n            gap: 3,\n            fontSize: 12,\n            color: metaColor,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {item.time ?? \"9:41\"}\n          {outgoing && <DoubleCheck color={metaColor} />}\n        </span>\n      </div>\n      <BubbleTail side={outgoing ? \"right\" : \"left\"} color={tailColor} />\n    </div>\n  );\n\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"flex-end\",\n        justifyContent: outgoing ? \"flex-end\" : \"flex-start\",\n        gap: 7,\n        width: \"100%\",\n        opacity,\n        transform: `translateY(${translateY}px) scale(${scale})`,\n        transformOrigin: outgoing ? \"bottom right\" : \"bottom left\",\n      }}\n    >\n      {!outgoing && contact !== undefined && (\n        <Avatar contact={contact} size={30} />\n      )}\n      {bubble}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/telegram-chat-flow.tsx"
    }
  ],
  "type": "registry:component"
}
