# Listicle Outreach

> Track the listicles AI answers cite and pitch their owners, with stats, a prospect table, a detail sheet and an email modal.

A shadcn/ui block from the [@notra](https://ui.usenotra.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/notra/listicle-outreach).

- Page, with a live preview: https://shadcn.exchange/r/notra/listicle-outreach
- Screenshot: https://shadcn.exchange/shot/notra/listicle-outreach/light.png
- Registry item: https://ui.usenotra.com/r/listicle-outreach.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/notra/listicle-outreach.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@xyflow/react`, `cn`, `lucide-react`, `sonner`
- Registry items: `avatar`, `badge`, `button`, `dialog`, `input`, `sheet`, `https://ui.usenotra.com/r/notra-table.json`, `tabs`, `textarea`

## Files

### `registry/notra/outreach/components/outreach-email-dialog.tsx`

```tsx
"use client";

import {
  DicesIcon,
  LoaderCircleIcon,
  SendIcon,
  WandSparklesIcon,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";

import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";

import { WorkflowLogo } from "../../workflow/components/workflow-logo";
import { findModel } from "../../workflow/lib/workflow";
import {
  OUTREACH_MODEL,
  OUTREACH_REWRITE_MS,
  OUTREACH_SEND_MS,
} from "../constants/outreach";
import { fillTemplate } from "../lib/outreach";
import type {
  OutreachDraft,
  OutreachEmailComposerProps,
  OutreachEmailDialogProps,
  OutreachVariable,
} from "../types/outreach";

const EmailComposer = ({
  drafts,
  model,
  onSend,
  prospect,
  variables,
}: OutreachEmailComposerProps) => {
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const timers = useRef<number[]>([]);
  const draftIndex = useRef(0);
  const [subject, setSubject] = useState(drafts[0]?.subject ?? "");
  const [body, setBody] = useState(drafts[0]?.body ?? "");
  const [rewriting, setRewriting] = useState(false);
  const [sending, setSending] = useState(false);
  const modelInfo = findModel(model);
  const [firstName] = prospect.contact.name.trim().split(/\s+/);

  useEffect(
    () => () => {
      for (const timer of timers.current) {
        window.clearTimeout(timer);
      }
    },
    []
  );
  const filled: OutreachVariable[] = variables.map((variable) => {
    if (variable.key === "first_name") {
      return { ...variable, value: firstName || variable.value };
    }
    if (variable.key === "listicle_title") {
      return { ...variable, value: prospect.title };
    }
    return variable;
  });

  const insertVariable = (key: string) => {
    const textarea = textareaRef.current;
    const token = `{{${key}}}`;
    const start = textarea?.selectionStart ?? body.length;
    const end = textarea?.selectionEnd ?? body.length;
    setBody(`${body.slice(0, start)}${token}${body.slice(end)}`);
    requestAnimationFrame(() => {
      textarea?.focus();
      textarea?.setSelectionRange(start + token.length, start + token.length);
    });
  };

  const rewrite = () => {
    setRewriting(true);
    const timer = window.setTimeout(() => {
      const next = (draftIndex.current + 1) % drafts.length;
      draftIndex.current = next;
      setSubject(drafts[next]?.subject ?? subject);
      setBody(drafts[next]?.body ?? body);
      setRewriting(false);
    }, OUTREACH_REWRITE_MS);
    timers.current.push(timer);
  };

  const send = () => {
    setSending(true);
    const draft: OutreachDraft = {
      body: fillTemplate(body, filled),
      subject: fillTemplate(subject, filled),
      to: prospect.contact.email,
    };
    const timer = window.setTimeout(() => {
      setSending(false);
      onSend?.(prospect, draft);
    }, OUTREACH_SEND_MS);
    timers.current.push(timer);
  };

  return (
    <>
      <DialogHeader>
        <DialogTitle className="pr-8 break-all">{`Pitch ${prospect.domain}`}</DialogTitle>
        <DialogDescription className="break-words">
          {`Ask ${prospect.contact.name} to add you to “${prospect.title}”.`}
        </DialogDescription>
      </DialogHeader>
      <div className="grid gap-3">
        <div className="grid gap-1.5">
          <label
            className="text-sm leading-none font-medium"
            htmlFor="outreach-to"
          >
            To
          </label>
          <Input
            id="outreach-to"
            readOnly
            value={`${prospect.contact.name} <${prospect.contact.email}>`}
          />
        </div>
        <div className="grid gap-1.5">
          <label
            className="text-sm leading-none font-medium"
            htmlFor="outreach-subject"
          >
            Subject
          </label>
          <Input
            id="outreach-subject"
            onChange={(event) => setSubject(event.target.value)}
            value={subject}
          />
        </div>
        <Tabs defaultValue="write">
          <div className="flex items-center justify-between gap-2">
            <TabsList>
              <TabsTrigger value="write">Write</TabsTrigger>
              <TabsTrigger value="preview">Preview</TabsTrigger>
            </TabsList>
            <Button
              disabled={drafts.length < 2 || rewriting}
              onClick={rewrite}
              size="sm"
              variant="outline"
            >
              {rewriting && (
                <LoaderCircleIcon
                  className="animate-spin"
                  data-icon="inline-start"
                />
              )}
              {!rewriting && modelInfo && (
                <WorkflowLogo className="size-3.5" name={modelInfo.logo} />
              )}
              {!(rewriting || modelInfo) && (
                <WandSparklesIcon data-icon="inline-start" />
              )}
              {rewriting ? "Rewriting" : "Rewrite"}
            </Button>
          </div>
          <TabsContent className="grid gap-2" value="write">
            <Textarea
              aria-label="Email body"
              className="min-h-40 font-mono text-xs leading-5 sm:min-h-56"
              onChange={(event) => setBody(event.target.value)}
              ref={textareaRef}
              value={body}
            />
            <div className="flex flex-wrap items-center gap-1.5">
              <span className="text-muted-foreground text-xs">Insert</span>
              {filled.map((variable) => (
                <Button
                  key={variable.key}
                  onClick={() => insertVariable(variable.key)}
                  size="xs"
                  title={variable.value}
                  variant="outline"
                >
                  {variable.label}
                </Button>
              ))}
            </div>
          </TabsContent>
          <TabsContent value="preview">
            <div className="border-table-card-border bg-table-card rounded-xl border text-[0.8125rem]">
              <div className="text-muted-foreground border-table-card-border grid gap-0.5 border-b px-3 py-2 text-xs">
                <span>
                  To:{" "}
                  <span className="text-foreground">
                    {prospect.contact.email}
                  </span>
                </span>
                <span>
                  Subject:{" "}
                  <span className="text-foreground font-medium">
                    {fillTemplate(subject, filled)}
                  </span>
                </span>
              </div>
              <p className="max-h-64 overflow-y-auto px-3 py-3 leading-6 whitespace-pre-wrap">
                {fillTemplate(body, filled)}
              </p>
            </div>
          </TabsContent>
        </Tabs>
      </div>
      <DialogFooter className="items-center sm:justify-between">
        <span className="text-muted-foreground flex min-w-0 items-center gap-1.5 text-xs">
          <DicesIcon className="size-3.5 shrink-0 text-violet-500" />
          <span className="truncate">{`Written by ${modelInfo?.label ?? model}`}</span>
        </span>
        <div className="flex gap-2">
          <DialogClose disabled={sending} render={<Button variant="outline" />}>
            Cancel
          </DialogClose>
          <Button disabled={sending} onClick={send}>
            {sending ? (
              <LoaderCircleIcon
                className="animate-spin"
                data-icon="inline-start"
              />
            ) : (
              <SendIcon data-icon="inline-start" />
            )}
            {sending ? "Sending" : "Send pitch"}
          </Button>
        </div>
      </DialogFooter>
    </>
  );
};

export const OutreachEmailDialog = ({
  drafts,
  model = OUTREACH_MODEL,
  onSend,
  prospect,
  variables,
  ...props
}: OutreachEmailDialogProps) => (
  <Dialog {...props}>
    <DialogContent
      className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-xl"
      data-slot="outreach-email-dialog"
    >
      {prospect && (
        <EmailComposer
          drafts={drafts}
          key={prospect.id}
          model={model}
          onSend={onSend}
          prospect={prospect}
          variables={variables}
        />
      )}
    </DialogContent>
  </Dialog>
);
```

### `registry/notra/outreach/components/outreach-prospect-sheet.tsx`

```tsx
"use client";

import { CheckIcon, ExternalLinkIcon, MailIcon } from "lucide-react";

import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
} from "@/components/ui/sheet";

import {
  WORKFLOW_LOGOS,
  WorkflowLogo,
} from "../../workflow/components/workflow-logo";
import { WORKFLOW_SECTION_TITLE_CLASS } from "../../workflow/constants/workflow-ui";
import { getInitials } from "../lib/outreach";
import type { OutreachProspectSheetProps } from "../types/outreach";
import { OutreachSequence } from "./outreach-sequence";
import { OutreachStatusBadge } from "./outreach-status";

export const OutreachProspectSheet = ({
  onDraft,
  onMarkListed,
  prospect,
  ...props
}: OutreachProspectSheetProps) => (
  <Sheet {...props}>
    <SheetContent
      className="gap-0 sm:max-w-md"
      data-slot="outreach-prospect-sheet"
    >
      {prospect && (
        <>
          <SheetHeader className="border-foreground/10 gap-2 border-b p-4 pr-12">
            <OutreachStatusBadge status={prospect.status} />
            <SheetTitle className="text-base leading-snug break-words">
              {prospect.title}
            </SheetTitle>
            <SheetDescription className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
              <a
                className="hover:text-foreground inline-flex min-w-0 items-center gap-1 break-all underline-offset-4 hover:underline"
                href={prospect.url}
                rel="noreferrer"
                target="_blank"
              >
                {prospect.domain}
                <ExternalLinkIcon className="size-3 shrink-0" />
              </a>
              <span className="whitespace-nowrap">
                · DR {prospect.domainRating}
              </span>
              <span className="whitespace-nowrap">· {prospect.updatedAt}</span>
            </SheetDescription>
          </SheetHeader>
          <div className="flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto p-4">
            <section className="flex flex-col gap-2">
              <h3 className={WORKFLOW_SECTION_TITLE_CLASS}>
                Cited in AI answers
              </h3>
              <ul className="border-table-card-border bg-table-card divide-table-row-border divide-y rounded-xl border">
                {prospect.citations.map((citation) => (
                  <li
                    className="flex items-center gap-2.5 px-3 py-2 text-[0.8125rem]"
                    key={citation.engine}
                  >
                    <WorkflowLogo name={citation.engine} />
                    <span className="flex-1">
                      {WORKFLOW_LOGOS[citation.engine].label}
                    </span>
                    <span className="text-muted-foreground text-xs tabular-nums">
                      {citation.prompts} prompts
                    </span>
                  </li>
                ))}
              </ul>
            </section>
            <section className="flex flex-col gap-2">
              <h3 className={WORKFLOW_SECTION_TITLE_CLASS}>
                Who's on the list
              </h3>
              <ol className="flex flex-col gap-1">
                {prospect.competitors
                  .toSorted((a, b) => a.position - b.position)
                  .map((competitor) => (
                    <li
                      className="flex items-center gap-2.5 text-[0.8125rem]"
                      key={competitor.name}
                    >
                      <span className="bg-muted text-muted-foreground flex size-5 items-center justify-center rounded font-mono text-[0.6875rem]">
                        {competitor.position}
                      </span>
                      {competitor.name}
                    </li>
                  ))}
              </ol>
            </section>
            <section className="flex flex-col gap-2">
              <h3 className={WORKFLOW_SECTION_TITLE_CLASS}>Contact</h3>
              <div className="flex items-center gap-3">
                <Avatar>
                  <AvatarFallback>
                    {getInitials(prospect.contact.name)}
                  </AvatarFallback>
                </Avatar>
                <div className="flex min-w-0 flex-col">
                  <span className="text-[0.8125rem] font-medium">
                    {prospect.contact.name}
                    <span className="text-muted-foreground font-normal">
                      {" "}
                      · {prospect.contact.role}
                    </span>
                  </span>
                  <span className="text-muted-foreground truncate text-xs">
                    {prospect.contact.email} · from {prospect.contact.source}
                  </span>
                </div>
              </div>
            </section>
            <section className="flex flex-col gap-2">
              <h3 className={WORKFLOW_SECTION_TITLE_CLASS}>Sequence</h3>
              {prospect.sequence.length > 0 ? (
                <OutreachSequence steps={prospect.sequence} />
              ) : (
                <p className="text-muted-foreground text-xs">
                  No emails yet. Draft a pitch to start the sequence.
                </p>
              )}
            </section>
          </div>
          <SheetFooter className="border-foreground/10 flex-row justify-end border-t p-3">
            {prospect.status !== "listed" && (
              <Button
                onClick={() => onMarkListed?.(prospect)}
                variant="outline"
              >
                <CheckIcon data-icon="inline-start" />
                Mark as listed
              </Button>
            )}
            <Button onClick={() => onDraft?.(prospect)}>
              <MailIcon data-icon="inline-start" />
              Draft pitch
            </Button>
          </SheetFooter>
        </>
      )}
    </SheetContent>
  </Sheet>
);
```

### `registry/notra/outreach/components/outreach-prospect-table.tsx`

```tsx
"use client";

import { cn } from "cn";
import { MailIcon } from "lucide-react";

import { Button } from "@/components/ui/button";

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "../../table/components/table";
import { OUTREACH_MAX_COMPETITORS } from "../constants/outreach";
import type { OutreachProspectTableProps } from "../types/outreach";
import { OutreachCitations, OutreachStatusBadge } from "./outreach-status";

export const OutreachProspectTable = ({
  className,
  onDraft,
  emptyLabel = "No listicles yet.",
  onOpen,
  prospects,
  ...props
}: OutreachProspectTableProps) => (
  <div
    className={cn("min-w-0", className)}
    data-slot="outreach-prospect-table"
    {...props}
  >
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Listicle</TableHead>
          <TableHead className="hidden md:table-cell">Cited by</TableHead>
          <TableHead className="hidden lg:table-cell">
            Competitors listed
          </TableHead>
          <TableHead className="hidden text-right sm:table-cell">DR</TableHead>
          <TableHead className="hidden sm:table-cell">Status</TableHead>
          <TableHead className="relative w-0">
            <span className="sr-only">Actions</span>
          </TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {prospects.length === 0 && (
          <TableRow>
            <TableCell
              className="text-muted-foreground h-24 text-center"
              colSpan={6}
            >
              {emptyLabel}
            </TableCell>
          </TableRow>
        )}
        {prospects.map((prospect) => (
          <TableRow
            className={cn(onOpen && "cursor-pointer")}
            data-status={prospect.status}
            key={prospect.id}
            onClick={() => onOpen?.(prospect)}
          >
            <TableCell className="w-full max-w-0 sm:w-auto sm:max-w-72">
              <button
                className="flex w-full min-w-0 flex-col text-left outline-none focus-visible:underline"
                onClick={(event) => {
                  event.stopPropagation();
                  onOpen?.(prospect);
                }}
                type="button"
              >
                <span className="truncate font-medium">{prospect.title}</span>
                <span className="text-muted-foreground truncate text-xs">
                  {prospect.domain}
                </span>
                <OutreachStatusBadge
                  className="mt-1.5 mb-1 sm:hidden"
                  status={prospect.status}
                />
              </button>
            </TableCell>
            <TableCell className="hidden md:table-cell">
              <OutreachCitations citations={prospect.citations} />
            </TableCell>
            <TableCell className="hidden lg:table-cell">
              <span className="flex flex-wrap gap-1">
                {prospect.competitors
                  .slice(0, OUTREACH_MAX_COMPETITORS)
                  .map((competitor) => (
                    <span
                      className={cn(
                        "bg-muted rounded-md px-1.5 py-0.5 text-[0.6875rem]",
                        competitor.name === "Notra" &&
                          "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
                      )}
                      key={competitor.name}
                    >
                      #{competitor.position} {competitor.name}
                    </span>
                  ))}
                {prospect.competitors.length > OUTREACH_MAX_COMPETITORS && (
                  <span className="text-muted-foreground px-1 py-0.5 text-[0.6875rem]">
                    +{prospect.competitors.length - OUTREACH_MAX_COMPETITORS}
                  </span>
                )}
              </span>
            </TableCell>
            <TableCell className="hidden text-right font-mono text-xs tabular-nums sm:table-cell">
              {prospect.domainRating}
            </TableCell>
            <TableCell className="hidden sm:table-cell">
              <OutreachStatusBadge status={prospect.status} />
            </TableCell>
            <TableCell>
              {(prospect.status === "found" ||
                prospect.status === "drafted") && (
                <Button
                  onClick={(event) => {
                    event.stopPropagation();
                    onDraft?.(prospect);
                  }}
                  size="xs"
                  variant="outline"
                >
                  <MailIcon data-icon="inline-start" />
                  Pitch
                </Button>
              )}
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  </div>
);
```

### `registry/notra/outreach/components/outreach-sequence.tsx`

```tsx
import { cn } from "cn";

import { OUTREACH_STEP_STATUSES } from "../constants/outreach";
import type { OutreachSequenceProps } from "../types/outreach";

export const OutreachSequence = ({
  className,
  steps,
  ...props
}: OutreachSequenceProps) => (
  <ol
    className={cn("flex flex-col", className)}
    data-slot="outreach-sequence"
    {...props}
  >
    {steps.map((step, index) => (
      <li
        className="relative grid grid-cols-[0.75rem_minmax(0,1fr)_auto] gap-3 pb-4 last:pb-0"
        data-status={step.status}
        key={step.id}
      >
        {index < steps.length - 1 && (
          <span
            aria-hidden="true"
            className="bg-border absolute top-4 bottom-0 left-[0.3125rem] w-px"
          />
        )}
        <span
          aria-hidden="true"
          className={cn(
            "mt-1 size-2.5 rounded-full",
            OUTREACH_STEP_STATUSES[step.status].className
          )}
        />
        <span className="flex min-w-0 flex-col">
          <span className="truncate text-[0.8125rem] font-medium">
            {step.subject}
          </span>
          <span className="text-muted-foreground text-xs">
            {`Day ${step.day} · ${OUTREACH_STEP_STATUSES[step.status].label}`}
          </span>
        </span>
        <span className="text-muted-foreground text-xs tabular-nums">
          {step.at}
        </span>
      </li>
    ))}
  </ol>
);
```

### `registry/notra/outreach/components/outreach-stats.tsx`

```tsx
import {
  WorkflowShell,
  WorkflowShellCard,
} from "../../workflow/components/workflow-shell";
import type { OutreachStatsProps } from "../types/outreach";

export const OutreachStats = ({
  className,
  stats,
  ...props
}: OutreachStatsProps) =>
  stats.length > 0 ? (
    <WorkflowShell className={className} data-slot="outreach-stats" {...props}>
      <WorkflowShellCard className="bg-table-card-border grid grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] gap-px overflow-hidden">
        {stats.map((stat) => (
          <div
            className="bg-table-card flex min-w-0 flex-col gap-1 p-4"
            key={stat.label}
          >
            <span className="text-muted-foreground truncate text-xs">
              {stat.label}
            </span>
            <span
              className="truncate text-2xl leading-tight font-semibold tracking-tight tabular-nums sm:text-3xl"
              title={stat.value}
            >
              {stat.value}
            </span>
            <span className="text-muted-foreground truncate text-[0.6875rem]">
              {stat.hint}
            </span>
          </div>
        ))}
      </WorkflowShellCard>
    </WorkflowShell>
  ) : null;
```

### `registry/notra/outreach/components/outreach-status.tsx`

```tsx
import { cn } from "cn";

import { Badge } from "@/components/ui/badge";

import {
  WORKFLOW_LOGOS,
  WorkflowLogo,
} from "../../workflow/components/workflow-logo";
import { OUTREACH_STATUSES } from "../constants/outreach";
import type {
  OutreachCitationsProps,
  OutreachStatusBadgeProps,
} from "../types/outreach";

export const OutreachStatusBadge = ({
  className,
  status,
  ...props
}: OutreachStatusBadgeProps) => (
  <Badge
    className={cn("gap-1.5", OUTREACH_STATUSES[status].className, className)}
    data-slot="outreach-status-badge"
    data-status={status}
    variant="outline"
    {...props}
  >
    <span aria-hidden="true" className="size-1.5 rounded-full bg-current" />
    {OUTREACH_STATUSES[status].label}
  </Badge>
);

export const OutreachCitations = ({
  citations,
  className,
  ...props
}: OutreachCitationsProps) => {
  const prompts = citations.reduce(
    (sum, citation) => sum + citation.prompts,
    0
  );
  return (
    <span
      className={cn("inline-flex items-center gap-2", className)}
      data-slot="outreach-citations"
      title={citations
        .map(
          (citation) =>
            `${WORKFLOW_LOGOS[citation.engine].label}: ${citation.prompts} prompts`
        )
        .join(", ")}
      {...props}
    >
      <span className="flex -space-x-1">
        {citations.map((citation) => (
          <span
            className="bg-background ring-foreground/10 flex size-5 items-center justify-center rounded-full ring-1"
            key={citation.engine}
          >
            <WorkflowLogo className="size-3" name={citation.engine} />
          </span>
        ))}
      </span>
      <span className="text-muted-foreground text-xs tabular-nums">
        {prompts} prompts
      </span>
    </span>
  );
};
```

### `registry/notra/outreach/constants/outreach.ts`

```ts
import type {
  OutreachDraft,
  OutreachProspect,
  OutreachStat,
  OutreachStatus,
  OutreachStatusConfig,
  OutreachStepStatus,
  OutreachVariable,
} from "../types/outreach";

export const OUTREACH_STATUSES: Record<OutreachStatus, OutreachStatusConfig> = {
  declined: { className: "text-muted-foreground", label: "Declined" },
  drafted: {
    className: "text-violet-600 dark:text-violet-300",
    label: "Drafted",
  },
  found: { className: "text-muted-foreground", label: "Found" },
  listed: {
    className: "text-emerald-600 dark:text-emerald-400",
    label: "Listed",
  },
  opened: { className: "text-sky-600 dark:text-sky-400", label: "Opened" },
  replied: {
    className: "text-amber-600 dark:text-amber-400",
    label: "Replied",
  },
  sent: { className: "text-blue-600 dark:text-blue-400", label: "Sent" },
};

export const OUTREACH_STEP_STATUSES: Record<
  OutreachStepStatus,
  OutreachStatusConfig
> = {
  cancelled: { className: "bg-muted-foreground/40", label: "Cancelled" },
  opened: { className: "bg-sky-500", label: "Opened" },
  replied: { className: "bg-amber-500", label: "Replied" },
  scheduled: {
    className: "bg-background ring-1 ring-foreground/25",
    label: "Scheduled",
  },
  sent: { className: "bg-blue-500", label: "Sent" },
};

export const OUTREACH_MODEL = "gpt-5";

export const OUTREACH_STATS: OutreachStat[] = [
  {
    hint: "Cited by an AI answer this month",
    label: "Listicles found",
    value: "37",
  },
  { hint: "12 waiting for a follow-up", label: "Pitches sent", value: "21" },
  { hint: "Industry average is 8%", label: "Reply rate", value: "28.6%" },
  { hint: "+4 citations in ChatGPT since", label: "Now listed", value: "5" },
];

export const OUTREACH_PROSPECTS: OutreachProspect[] = [
  {
    citations: [
      { engine: "openai", prompts: 9 },
      { engine: "perplexity", prompts: 6 },
      { engine: "gemini", prompts: 3 },
    ],
    competitors: [
      { name: "Beamer", position: 1 },
      { name: "LaunchNotes", position: 2 },
      { name: "Headway", position: 4 },
    ],
    contact: {
      email: "maya@example.com",
      name: "Maya Lindqvist",
      role: "Editor",
      source: "Author bio",
    },
    domain: "saasbase.dev",
    domainRating: 71,
    id: "saasbase",
    sequence: [
      {
        at: "Oct 20",
        day: 0,
        id: "s1",
        status: "opened",
        subject: "Notra for your changelog roundup",
      },
      {
        at: "Oct 23",
        day: 3,
        id: "s2",
        status: "replied",
        subject: "Re: Notra for your changelog roundup",
      },
    ],
    status: "replied",
    title: "The 12 best changelog tools in 2026",
    updatedAt: "2h ago",
    url: "https://saasbase.dev/best-changelog-tools",
  },
  {
    citations: [
      { engine: "perplexity", prompts: 7 },
      { engine: "openai", prompts: 2 },
    ],
    competitors: [
      { name: "LaunchNotes", position: 1 },
      { name: "Beamer", position: 3 },
    ],
    contact: {
      email: "jonas@example.com",
      name: "Jonas Meyer",
      role: "Founder",
      source: "Contact page",
    },
    domain: "devtools.fm",
    domainRating: 64,
    id: "devtools",
    sequence: [
      {
        at: "Oct 22",
        day: 0,
        id: "s1",
        status: "opened",
        subject: "A release-notes tool for your list",
      },
      {
        at: "Oct 25",
        day: 3,
        id: "s2",
        status: "scheduled",
        subject: "Quick follow-up",
      },
      {
        at: "Oct 29",
        day: 7,
        id: "s3",
        status: "scheduled",
        subject: "Last note from me",
      },
    ],
    status: "opened",
    title: "Top release notes apps for dev teams",
    updatedAt: "Yesterday",
    url: "https://devtools.fm/top-release-notes-apps",
  },
  {
    citations: [
      { engine: "gemini", prompts: 5 },
      { engine: "grok", prompts: 2 },
    ],
    competitors: [
      { name: "Headway", position: 2 },
      { name: "Beamer", position: 5 },
    ],
    contact: {
      email: "editor@example.com",
      name: "Priya Raman",
      role: "Managing editor",
      source: "Masthead",
    },
    domain: "productledhq.com",
    domainRating: 58,
    id: "productled",
    sequence: [
      {
        at: "Oct 24",
        day: 0,
        id: "s1",
        status: "sent",
        subject: "Changelog automation for your guide",
      },
      {
        at: "Oct 27",
        day: 3,
        id: "s2",
        status: "scheduled",
        subject: "Quick follow-up",
      },
    ],
    status: "sent",
    title: "9 product update tools that save PMs time",
    updatedAt: "Yesterday",
    url: "https://productledhq.com/product-update-tools",
  },
  {
    citations: [{ engine: "openai", prompts: 4 }],
    competitors: [{ name: "LaunchNotes", position: 3 }],
    contact: {
      email: "sam@example.com",
      name: "Sam Okafor",
      role: "Writer",
      source: "Author bio",
    },
    domain: "shipfaster.io",
    domainRating: 47,
    id: "shipfaster",
    sequence: [],
    status: "drafted",
    title: "How to write release notes people read (+ 7 tools)",
    updatedAt: "3h ago",
    url: "https://shipfaster.io/release-notes-guide",
  },
  {
    citations: [
      { engine: "perplexity", prompts: 3 },
      { engine: "claude", prompts: 2 },
    ],
    competitors: [
      { name: "Beamer", position: 1 },
      { name: "Headway", position: 2 },
    ],
    contact: {
      email: "hello@example.com",
      name: "Lea Fischer",
      role: "Owner",
      source: "WHOIS",
    },
    domain: "indiestack.co",
    domainRating: 39,
    id: "indiestack",
    sequence: [],
    status: "found",
    title: "Best tools for indie SaaS founders: changelogs",
    updatedAt: "Just now",
    url: "https://indiestack.co/changelog-tools",
  },
  {
    citations: [{ engine: "openai", prompts: 6 }],
    competitors: [
      { name: "Notra", position: 3 },
      { name: "Beamer", position: 1 },
    ],
    contact: {
      email: "tom@example.com",
      name: "Tom Becker",
      role: "Editor",
      source: "Author bio",
    },
    domain: "growthnotes.com",
    domainRating: 66,
    id: "growthnotes",
    sequence: [
      {
        at: "Oct 2",
        day: 0,
        id: "s1",
        status: "replied",
        subject: "Notra for your roundup",
      },
    ],
    status: "listed",
    title: "The changelog stack for growth teams",
    updatedAt: "Oct 9",
    url: "https://growthnotes.com/changelog-stack",
  },
];

export const OUTREACH_VARIABLES: OutreachVariable[] = [
  { key: "first_name", label: "First name", value: "Maya" },
  {
    key: "listicle_title",
    label: "Listicle",
    value: "The 12 best changelog tools in 2026",
  },
  { key: "company", label: "Company", value: "Notra" },
  {
    key: "proof_point",
    label: "Proof point",
    value: "a weekly changelog takes minutes instead of an afternoon",
  },
];

export const OUTREACH_DRAFTS: OutreachDraft[] = [
  {
    body: `Hi {{first_name}},

I read "{{listicle_title}}" while researching how teams pick a changelog tool. It's one of the pages ChatGPT and Perplexity quote most for that question.

I work on {{company}}. It turns merged pull requests into changelog posts and release notes, so {{proof_point}}.

Would you consider adding it to the list? Happy to send a demo account, screenshots or anything else that makes it easy.

Thanks,
Alex`,
    subject: "{{company}} for {{listicle_title}}",
    to: "",
  },
  {
    body: `Hi {{first_name}},

Quick one: your roundup "{{listicle_title}}" is cited in AI answers for "best changelog tool", and {{company}} is missing from it.

We're an AI changelog writer for dev teams; {{proof_point}}. If it's a fit, I can send everything you need for a write-up, no strings attached.

Best,
Alex`,
    subject: "A missing pick for {{listicle_title}}?",
    to: "",
  },
];

export const OUTREACH_MAX_COMPETITORS = 4;

export const OUTREACH_REWRITE_MS = 1200;

export const OUTREACH_SEND_MS = 900;

export const OUTREACH_PITCHABLE = new Set<OutreachStatus>([
  "declined",
  "drafted",
  "found",
]);
```

### `registry/notra/outreach/examples/listicle-outreach.tsx`

```tsx
"use client";

import { useState } from "react";
import { toast } from "sonner";

import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";

import { OutreachEmailDialog } from "../components/outreach-email-dialog";
import { OutreachProspectSheet } from "../components/outreach-prospect-sheet";
import { OutreachProspectTable } from "../components/outreach-prospect-table";
import { OutreachStats } from "../components/outreach-stats";
import {
  OUTREACH_DRAFTS,
  OUTREACH_PITCHABLE,
  OUTREACH_PROSPECTS,
  OUTREACH_STATS,
  OUTREACH_VARIABLES,
} from "../constants/outreach";
import type { OutreachFilter, OutreachProspect } from "../types/outreach";

const FILTERS: OutreachFilter[] = [
  { label: "All", statuses: [] },
  { label: "To pitch", statuses: ["found", "drafted"] },
  { label: "In sequence", statuses: ["sent", "opened", "replied"] },
  { label: "Won", statuses: ["listed"] },
];

export default function ListicleOutreach() {
  const [prospects, setProspects] = useState(OUTREACH_PROSPECTS);
  const [filter, setFilter] = useState("All");
  const [activeId, setActiveId] = useState<string>();
  const [sheetOpen, setSheetOpen] = useState(false);
  const [dialogOpen, setDialogOpen] = useState(false);
  const active = prospects.find((prospect) => prospect.id === activeId);
  const statuses = new Set(
    FILTERS.find((item) => item.label === filter)?.statuses
  );
  const visible =
    statuses.size === 0
      ? prospects
      : prospects.filter((prospect) => statuses.has(prospect.status));

  const updateProspect = (
    id: string,
    update: (prospect: OutreachProspect) => Partial<OutreachProspect>
  ) =>
    setProspects((current) =>
      current.map((prospect) =>
        prospect.id === id
          ? { ...prospect, ...update(prospect), updatedAt: "Just now" }
          : prospect
      )
    );

  const openSheet = (prospect: OutreachProspect) => {
    setActiveId(prospect.id);
    setSheetOpen(true);
  };

  const openDialog = (prospect: OutreachProspect) => {
    setActiveId(prospect.id);
    setSheetOpen(false);
    setDialogOpen(true);
  };

  return (
    <div className="flex w-full min-w-0 flex-col gap-4">
      <div className="flex flex-col gap-0.5">
        <h2 className="text-base font-semibold">Listicle outreach</h2>
        <p className="text-muted-foreground text-sm">
          Best-of pages that AI answers quote, and where you stand on each.
        </p>
      </div>
      <OutreachStats stats={OUTREACH_STATS} />
      <Tabs onValueChange={(value) => setFilter(String(value))} value={filter}>
        <TabsList>
          {FILTERS.map((item) => (
            <TabsTrigger key={item.label} value={item.label}>
              {item.label}
            </TabsTrigger>
          ))}
        </TabsList>
      </Tabs>
      <OutreachProspectTable
        emptyLabel="No listicles match this filter."
        onDraft={openDialog}
        onOpen={openSheet}
        prospects={visible}
      />
      <OutreachProspectSheet
        onDraft={openDialog}
        onMarkListed={(prospect) => {
          updateProspect(prospect.id, () => ({ status: "listed" }));
          toast.success(`${prospect.domain} marked as listed`);
        }}
        onOpenChange={setSheetOpen}
        open={sheetOpen}
        prospect={active}
      />
      <OutreachEmailDialog
        drafts={OUTREACH_DRAFTS}
        onOpenChange={setDialogOpen}
        onSend={(prospect, draft) => {
          updateProspect(prospect.id, (current) => ({
            sequence: [
              ...current.sequence,
              {
                at: "Just now",
                day: current.sequence.at(-1)?.day ?? 0,
                id: `s${current.sequence.length + 1}`,
                status: "sent",
                subject: draft.subject,
              },
            ],
            status: OUTREACH_PITCHABLE.has(current.status)
              ? "sent"
              : current.status,
          }));
          setDialogOpen(false);
          toast.success(`Sent “${draft.subject}” to ${prospect.contact.name}`);
        }}
        open={dialogOpen}
        prospect={active}
        variables={OUTREACH_VARIABLES}
      />
    </div>
  );
}
```

### `registry/notra/outreach/lib/outreach.ts`

```ts
import type { OutreachVariable } from "../types/outreach";

export const fillTemplate = (text: string, variables: OutreachVariable[]) =>
  text.replaceAll(
    /\{\{(\w+)\}\}/g,
    (match, key: string) =>
      variables.find((variable) => variable.key === key)?.value ?? match
  );

export const getInitials = (name: string) =>
  name
    .split(" ")
    .filter(Boolean)
    .slice(0, 2)
    .map((part) => Array.from(part)[0])
    .join("")
    .toUpperCase();
```

### `registry/notra/outreach/types/outreach.ts`

```ts
import type { ComponentProps, ReactNode } from "react";

import type {
  WorkflowLogoName,
  WorkflowOverlayProps,
} from "../../workflow/types/workflow";

export type OutreachStatus =
  | "found"
  | "drafted"
  | "sent"
  | "opened"
  | "replied"
  | "listed"
  | "declined";

export type OutreachStepStatus =
  | "sent"
  | "opened"
  | "replied"
  | "scheduled"
  | "cancelled";

export interface OutreachStatusConfig {
  className: string;
  label: string;
}

export interface OutreachCitation {
  engine: WorkflowLogoName;
  prompts: number;
}

export interface OutreachContact {
  email: string;
  name: string;
  role: string;
  source: string;
}

export interface OutreachCompetitor {
  name: string;
  position: number;
}

export interface OutreachSequenceStep {
  id: string;
  day: number;
  subject: string;
  status: OutreachStepStatus;
  at: string;
}

export interface OutreachProspect {
  id: string;
  title: string;
  url: string;
  domain: string;
  domainRating: number;
  status: OutreachStatus;
  citations: OutreachCitation[];
  competitors: OutreachCompetitor[];
  contact: OutreachContact;
  sequence: OutreachSequenceStep[];
  updatedAt: string;
}

export interface OutreachDraft {
  body: string;
  subject: string;
  to: string;
}

export interface OutreachVariable {
  key: string;
  label: string;
  value: string;
}

export interface OutreachStat {
  hint: string;
  label: string;
  value: string;
}

export type OutreachStatusBadgeProps = ComponentProps<"span"> & {
  status: OutreachStatus;
};

export type OutreachCitationsProps = ComponentProps<"span"> & {
  citations: OutreachCitation[];
};

export type OutreachProspectTableProps = ComponentProps<"div"> & {
  prospects: OutreachProspect[];
  emptyLabel?: ReactNode;
  onOpen?: (prospect: OutreachProspect) => void;
  onDraft?: (prospect: OutreachProspect) => void;
};

export type OutreachEmailDialogProps = WorkflowOverlayProps & {
  prospect?: OutreachProspect;
  drafts: OutreachDraft[];
  variables: OutreachVariable[];
  model?: string;
  onSend?: (prospect: OutreachProspect, draft: OutreachDraft) => void;
};

export type OutreachProspectSheetProps = WorkflowOverlayProps & {
  prospect?: OutreachProspect;
  onDraft?: (prospect: OutreachProspect) => void;
  onMarkListed?: (prospect: OutreachProspect) => void;
};

export type OutreachSequenceProps = ComponentProps<"ol"> & {
  steps: OutreachSequenceStep[];
};

export type OutreachStatsProps = ComponentProps<"div"> & {
  stats: OutreachStat[];
};

export type OutreachEmailComposerProps = Required<
  Pick<OutreachEmailDialogProps, "drafts" | "variables" | "model">
> &
  Pick<OutreachEmailDialogProps, "onSend"> & { prospect: OutreachProspect };

export interface OutreachFilter {
  label: string;
  statuses: OutreachStatus[];
}
```

### `registry/notra/workflow/components/workflow-logo.tsx`

```tsx
import { cn } from "cn";

import type {
  WorkflowLogoConfig,
  WorkflowLogoName,
  WorkflowLogoProps,
} from "../types/workflow";
import {
  AhrefsLogo,
  AnthropicLogo,
  BrowserbaseLogo,
  ClaudeLogo,
  ContextDevLogo,
  DeepSeekLogo,
  FirecrawlLogo,
  GeminiLogo,
  GitHubLogo,
  GmailLogo,
  GoogleLogo,
  GoogleSheetsLogo,
  GrokLogo,
  LinearLogo,
  MistralLogo,
  NotionLogo,
  OpenAILogo,
  PerplexityLogo,
  PostHogLogo,
  ResendLogo,
  SlackLogo,
  WebflowLogo,
  WordPressLogo,
} from "./workflow-logos";

export const WORKFLOW_LOGOS: Record<WorkflowLogoName, WorkflowLogoConfig> = {
  ahrefs: { label: "Ahrefs", logo: AhrefsLogo },
  anthropic: { label: "Anthropic", logo: AnthropicLogo },
  browserbase: { label: "Browserbase", logo: BrowserbaseLogo, tile: true },
  claude: { label: "Claude", logo: ClaudeLogo },
  deepseek: { label: "DeepSeek", logo: DeepSeekLogo },
  "context-dev": { label: "context.dev", logo: ContextDevLogo, tile: true },
  firecrawl: { label: "Firecrawl", logo: FirecrawlLogo },
  gemini: { label: "Gemini", logo: GeminiLogo },
  github: { label: "GitHub", logo: GitHubLogo },
  gmail: { label: "Gmail", logo: GmailLogo },
  google: { label: "Google", logo: GoogleLogo },
  "google-sheets": { label: "Google Sheets", logo: GoogleSheetsLogo },
  grok: { label: "Grok", logo: GrokLogo },
  linear: { label: "Linear", logo: LinearLogo },
  mistral: { label: "Mistral", logo: MistralLogo },
  notion: { label: "Notion", logo: NotionLogo },
  openai: { label: "OpenAI", logo: OpenAILogo },
  perplexity: { label: "Perplexity", logo: PerplexityLogo },
  posthog: { label: "PostHog", logo: PostHogLogo },
  resend: { label: "Resend", logo: ResendLogo },
  slack: { label: "Slack", logo: SlackLogo },
  webflow: { label: "Webflow", logo: WebflowLogo },
  wordpress: { label: "WordPress", logo: WordPressLogo },
};

export const WorkflowLogo = ({
  className,
  name,
  ...props
}: WorkflowLogoProps) => {
  const Logo = WORKFLOW_LOGOS[name].logo;
  return (
    <Logo
      className={cn("size-4 shrink-0 overflow-visible", className)}
      data-slot="workflow-logo"
      {...props}
    />
  );
};
```

### `registry/notra/workflow/components/workflow-logos.tsx`

```tsx
import type { SVGProps } from "react";

export const OpenAILogo = (props: SVGProps<SVGSVGElement>) => (
  <svg
    viewBox="105.31 105.31 400.39 400.39"
    fill="currentColor"
    aria-hidden="true"
    {...props}
  >
    <path
      fillRule="evenodd"
      clipRule="evenodd"
      d="M252.794 108.802C289.191 99.0484 326.265 110.305 351.148 135.135C385.113 126.072 422.85 134.862 449.492 161.505C476.136 188.149 484.925 225.888 475.862 259.85V259.854C500.696 284.735 511.95 321.81 502.198 358.207C492.447 394.602 464.161 421.084 430.215 430.217C421.083 464.162 394.603 492.448 358.206 502.199C321.812 511.951 284.734 500.693 259.852 475.864C225.887 484.927 188.15 476.137 161.507 449.495C134.864 422.851 126.073 385.111 135.136 351.149C110.304 326.266 99.0496 289.192 108.801 252.795C118.552 216.4 146.84 189.918 180.784 180.785C189.917 146.841 216.396 118.553 252.794 108.802ZM374.292 407.145C374.292 411.271 372.092 415.086 368.517 417.148L283.723 466.102C302.487 480.585 327.555 486.459 352.217 479.852C386.997 470.532 410.068 439.312 410.555 405.006V317.717C410.555 315.08 409.125 312.621 406.843 311.303L374.292 292.509V407.145ZM251.868 415.897C248.296 417.959 243.893 417.959 240.317 415.897L155.526 366.942C152.366 390.436 159.811 415.08 177.866 433.136H177.863C203.325 458.594 241.896 462.962 271.85 446.232L347.449 402.586C349.735 401.268 351.148 398.8 351.148 396.163V358.579L251.868 415.897ZM368.602 220.628C366.319 219.309 363.474 219.318 361.191 220.637L328.641 239.431L427.921 296.749C431.496 298.811 433.697 302.627 433.697 306.752V404.661C455.622 395.654 473.244 376.881 479.851 352.218C489.169 317.442 473.668 281.85 444.201 264.274L368.602 220.628ZM177.303 206.34C155.377 215.348 137.756 234.122 131.148 258.783C121.832 293.561 137.331 329.153 166.799 346.727L242.398 390.373C244.68 391.692 247.525 391.684 249.807 390.366L282.357 371.572L183.078 314.253C179.504 312.189 177.303 308.375 177.303 304.251V206.34ZM259.849 279.145V331.858L305.5 358.213L351.15 331.858V279.145L305.5 252.789L259.849 279.145ZM327.276 144.9C308.512 130.418 283.445 124.543 258.782 131.15C224.002 140.471 200.931 171.691 200.445 205.995V293.286C200.445 295.923 201.875 298.381 204.158 299.7L236.707 318.493V203.856C236.707 199.731 238.909 195.916 242.483 193.853L327.276 144.9ZM433.137 177.867C407.675 152.407 369.103 148.038 339.149 164.769L263.55 208.415C261.265 209.734 259.852 212.202 259.852 214.838V252.423L359.132 195.105C362.703 193.041 367.108 193.041 370.682 195.105L455.473 244.06C458.635 220.567 451.189 195.922 433.135 177.867H433.137Z"
    />
  </svg>
);

export const AnthropicLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg
    viewBox="0 3.52 24 16.48"
    fill="currentColor"
    aria-hidden="true"
    {...props}
  >
    <path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z" />
  </svg>
);

export const ClaudeLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 256 257" aria-hidden="true" {...props}>
    <path
      fill="#D97757"
      d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"
    />
  </svg>
);

export const GeminiLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 296 298" fill="none" aria-hidden="true" {...props}>
    <mask
      id="gemini-a"
      width="296"
      height="298"
      x="0"
      y="0"
      maskUnits="userSpaceOnUse"
      style={{ maskType: "alpha" }}
    >
      <path
        fill="#3186FF"
        d="M141.201 4.886c2.282-6.17 11.042-6.071 13.184.148l5.985 17.37a184.004 184.004 0 0 0 111.257 113.049l19.304 6.997c6.143 2.227 6.156 10.91.02 13.155l-19.35 7.082a184.001 184.001 0 0 0-109.495 109.385l-7.573 20.629c-2.241 6.105-10.869 6.121-13.133.025l-7.908-21.296a184 184 0 0 0-109.02-108.658l-19.698-7.239c-6.102-2.243-6.118-10.867-.025-13.132l20.083-7.467A183.998 183.998 0 0 0 133.291 26.28l7.91-21.394Z"
      />
    </mask>
    <g mask="url(#gemini-a)">
      <g filter="url(#gemini-b)">
        <ellipse cx="163" cy="149" fill="#3689FF" rx="196" ry="159" />
      </g>
      <g filter="url(#gemini-c)">
        <ellipse cx="33.5" cy="142.5" fill="#F6C013" rx="68.5" ry="72.5" />
      </g>
      <g filter="url(#gemini-d)">
        <ellipse cx="19.5" cy="148.5" fill="#F6C013" rx="68.5" ry="72.5" />
      </g>
      <g filter="url(#gemini-e)">
        <path
          fill="#FA4340"
          d="M194 10.5C172 82.5 65.5 134.333 22.5 135L144-66l50 76.5Z"
        />
      </g>
      <g filter="url(#gemini-f)">
        <path
          fill="#FA4340"
          d="M190.5-12.5C168.5 59.5 62 111.333 19 112L140.5-89l50 76.5Z"
        />
      </g>
      <g filter="url(#gemini-g)">
        <path
          fill="#14BB69"
          d="M194.5 279.5C172.5 207.5 66 155.667 23 155l121.5 201 50-76.5Z"
        />
      </g>
      <g filter="url(#gemini-h)">
        <path
          fill="#14BB69"
          d="M196.5 320.5C174.5 248.5 68 196.667 25 196l121.5 201 50-76.5Z"
        />
      </g>
    </g>
    <defs>
      <filter
        id="gemini-b"
        width="464"
        height="390"
        x="-69"
        y="-46"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="18"
        />
      </filter>
      <filter
        id="gemini-c"
        width="265"
        height="273"
        x="-99"
        y="6"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
      <filter
        id="gemini-d"
        width="265"
        height="273"
        x="-113"
        y="12"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
      <filter
        id="gemini-e"
        width="299.5"
        height="329"
        x="-41.5"
        y="-130"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
      <filter
        id="gemini-f"
        width="299.5"
        height="329"
        x="-45"
        y="-153"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
      <filter
        id="gemini-g"
        width="299.5"
        height="329"
        x="-41"
        y="91"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
      <filter
        id="gemini-h"
        width="299.5"
        height="329"
        x="-39"
        y="132"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
    </defs>
  </svg>
);

export const PerplexityLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="8.94 4 30.12 40" aria-hidden="true" {...props}>
    <path
      fill="none"
      stroke="#20808d"
      strokeLinecap="round"
      strokeLinejoin="round"
      d="M24 4.5v39M13.73 16.573v-9.99L24 16.573m0 14.5L13.73 41.417V27.01L24 16.573m0 0l10.27-9.99v9.99"
    />
    <path
      fill="none"
      stroke="#20808d"
      strokeLinecap="round"
      strokeLinejoin="round"
      d="M13.73 31.396H9.44V16.573h29.12v14.823h-4.29"
    />
    <path
      fill="none"
      stroke="#20808d"
      strokeLinecap="round"
      strokeLinejoin="round"
      d="M24 16.573L34.27 27.01v14.407L24 31.073"
    />
  </svg>
);

export const MistralLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 256 233" aria-hidden="true" {...props}>
    <path d="M186.18182 0h46.54545v46.54545h-46.54545z" />
    <path fill="#F7D046" d="M209.45454 0h46.54545v46.54545h-46.54545z" />
    <path d="M0 0h46.54545v46.54545H0zM0 46.54545h46.54545V93.0909H0zM0 93.09091h46.54545v46.54545H0zM0 139.63636h46.54545v46.54545H0zM0 186.18182h46.54545v46.54545H0z" />
    <path fill="#F7D046" d="M23.27273 0h46.54545v46.54545H23.27273z" />
    <path
      fill="#F2A73B"
      d="M209.45454 46.54545h46.54545V93.0909h-46.54545zM23.27273 46.54545h46.54545V93.0909H23.27273z"
    />
    <path d="M139.63636 46.54545h46.54545V93.0909h-46.54545z" />
    <path
      fill="#F2A73B"
      d="M162.90909 46.54545h46.54545V93.0909h-46.54545zM69.81818 46.54545h46.54545V93.0909H69.81818z"
    />
    <path
      fill="#EE792F"
      d="M116.36364 93.09091h46.54545v46.54545h-46.54545zM162.90909 93.09091h46.54545v46.54545h-46.54545zM69.81818 93.09091h46.54545v46.54545H69.81818z"
    />
    <path d="M93.09091 139.63636h46.54545v46.54545H93.09091z" />
    <path
      fill="#EB5829"
      d="M116.36364 139.63636h46.54545v46.54545h-46.54545z"
    />
    <path
      fill="#EE792F"
      d="M209.45454 93.09091h46.54545v46.54545h-46.54545zM23.27273 93.09091h46.54545v46.54545H23.27273z"
    />
    <path d="M186.18182 139.63636h46.54545v46.54545h-46.54545z" />
    <path
      fill="#EB5829"
      d="M209.45454 139.63636h46.54545v46.54545h-46.54545z"
    />
    <path d="M186.18182 186.18182h46.54545v46.54545h-46.54545z" />
    <path fill="#EB5829" d="M23.27273 139.63636h46.54545v46.54545H23.27273z" />
    <path
      fill="#EA3326"
      d="M209.45454 186.18182h46.54545v46.54545h-46.54545zM23.27273 186.18182h46.54545v46.54545H23.27273z"
    />
  </svg>
);

export const DeepSeekLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 3 24 17.66" aria-hidden="true" {...props}>
    <path
      fill="#4D6BFE"
      d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 0 1-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 0 0-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 0 1-.465.137 9.597 9.597 0 0 0-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 0 0 1.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 0 1 1.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 0 1 .415-.287.302.302 0 0 1 .2.288.306.306 0 0 1-.31.307.303.303 0 0 1-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 0 1-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 0 1 .016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 0 1-.254-.078.253.253 0 0 1-.114-.358c.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z"
    />
  </svg>
);

export const GrokLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg
    viewBox="0 0 1024 1024"
    fill="currentColor"
    aria-hidden="true"
    {...props}
  >
    <path d="M395.479 633.828L735.91 381.105C752.599 368.715 776.454 373.548 784.406 392.792C826.26 494.285 807.561 616.253 724.288 699.996C641.016 783.739 525.151 802.104 419.247 760.277L303.556 814.143C469.49 928.202 670.987 899.995 796.901 773.282C896.776 672.843 927.708 535.937 898.785 412.476L899.047 412.739C857.105 231.37 909.358 158.874 1016.4 10.6326C1018.93 7.11771 1021.47 3.60279 1024 0L883.144 141.651V141.212L395.392 633.916" />
    <path d="M325.226 695.251C206.128 580.84 226.662 403.776 328.285 301.668C403.431 226.097 526.549 195.254 634.026 240.596L749.454 186.994C728.657 171.88 702.007 155.623 671.424 144.2C533.19 86.9942 367.693 115.465 255.323 228.382C147.234 337.081 113.244 504.215 171.613 646.833C215.216 753.423 143.739 828.818 71.7385 904.916C46.2237 931.893 20.6216 958.87 0 987.429L325.139 695.339" />
  </svg>
);

export const BrowserbaseLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 200 200" fill="none" aria-hidden="true" {...props}>
    <rect x="30" y="36" width="134" height="124" fill="white" />
    <path d="M111.168 116.901H83.168V109.901H111.168V116.901Z" fill="#FF4500" />
    <path d="M111.168 86.208H83.168V79.208H111.168V86.208Z" fill="#FF4500" />
    <path
      fillRule="evenodd"
      clipRule="evenodd"
      d="M200 200H0V0H200V200ZM55.4453 147.815H128.678L145.259 131.234V111.891L131.441 98.0723L142.495 87.0186V69.0557L125.914 52.4756H55.4453V147.815Z"
      fill="#FF4500"
    />
  </svg>
);

export const ContextDevLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 180 180" fill="none" aria-hidden="true" {...props}>
    <rect width="180" height="180" fill="#2663EB" />
    <g transform="translate(22 45) scale(0.1658)" fill="#FFFFFF">
      <path
        fillRule="evenodd"
        d="M287.56,0H745.26A27.06,27.06,0,0,1,772.32,27.06V484.77A27.06,27.06,0,0,1,745.26,511.83H284.51A24.01,24.01,0,0,1,260.5,487.82V27.06A27.06,27.06,0,0,1,287.56,0ZM260.5,358.28a153.55,153.55,0,1,0,307.1,0a153.55,153.55,0,1,0,-307.1,0Z"
      />
      <circle cx="153.55" cy="377.64" r="153.55" />
    </g>
  </svg>
);

export const FirecrawlLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 50 72" fill="none" aria-hidden="true" {...props}>
    <path
      fill="#fa5d19"
      d="M41.715 23.193c-2.762.82-4.844 2.675-6.37 4.69-.327.432-1.01.107-.88-.423 2.92-12.007-.937-21.986-12.961-26.898a.803.803 0 0 0-1.085.937c5.47 21.961-17.537 20.109-14.63 45.005.05.427-.43.72-.78.47-1.09-.782-2.307-2.415-3.142-3.562a.502.502 0 0 0-.887.16c-.665 2.404-.98 4.67-.98 6.92 0 8.749 4.497 16.45 11.304 20.915.39.255.89-.11.758-.557a13.5 13.5 0 0 1-.563-3.697c0-.788.05-1.593.173-2.343.285-1.885.94-3.68 2.04-5.314 3.772-5.663 11.334-11.132 10.127-18.56-.078-.47.477-.78.827-.457 5.328 4.868 6.383 11.415 5.508 17.287-.075.51.564.782.887.382a11.6 11.6 0 0 1 2.892-2.587c.27-.168.63-.04.733.26.602 1.752 1.497 3.397 2.342 5.042a13.46 13.46 0 0 1 .905 9.982.502.502 0 0 0 .755.57C45.5 66.95 50 59.248 50 50.494c0-3.043-.532-6.025-1.54-8.82-2.112-5.862-7.472-10.264-6.117-17.904.065-.365-.273-.682-.628-.577"
    />
  </svg>
);

export const ResendLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg
    viewBox="450 450 900 900"
    fill="currentColor"
    aria-hidden="true"
    {...props}
  >
    <path d="M1000.46 450C1174.77 450 1278.43 553.669 1278.43 691.282C1278.43 828.896 1174.77 932.563 1000.46 932.563H912.382L1350 1350H1040.82L707.794 1033.48C683.944 1011.47 672.936 985.781 672.935 963.765C672.935 932.572 694.959 905.049 737.161 893.122L908.712 847.244C973.85 829.812 1018.81 779.353 1018.81 713.298C1018.8 632.567 952.745 585.78 871.095 585.78H450V450H1000.46Z" />
  </svg>
);

export const SlackLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 2447.6 2452.5" aria-hidden="true" {...props}>
    <g clipRule="evenodd" fillRule="evenodd">
      <path
        d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
        fill="#36c5f0"
      />
      <path
        d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
        fill="#2eb67d"
      />
      <path
        d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
        fill="#ecb22e"
      />
      <path
        d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
        fill="#e01e5a"
      />
    </g>
  </svg>
);

export const GmailLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 64.72 512 384.1" aria-hidden="true" {...props}>
    <g fill="none" fillRule="evenodd">
      <g fillRule="nonzero">
        <path
          fill="#4285f4"
          d="M34.91 448.818h81.454V251L0 163.727V413.91c0 19.287 15.622 34.91 34.91 34.91z"
        />
        <path
          fill="#34a853"
          d="M395.636 448.818h81.455c19.287 0 34.909-15.622 34.909-34.909V163.727L395.636 251z"
        />
        <path
          fill="#fbbc04"
          d="M395.636 99.727V251L512 163.727v-46.545c0-43.142-49.25-67.782-83.782-41.891z"
        />
      </g>
      <path
        fill="#ea4335"
        d="M116.364 251V99.727L256 204.455 395.636 99.727V251L256 355.727z"
      />
      <path
        fill="#c5221f"
        fillRule="nonzero"
        d="M0 117.182v46.545L116.364 251V99.727L83.782 75.291C49.25 49.4 0 74.04 0 117.18z"
      />
    </g>
  </svg>
);

export const NotionLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 256 268" aria-hidden="true" {...props}>
    <path
      fill="#FFF"
      d="M16.092 11.538 164.09.608c18.179-1.56 22.85-.508 34.28 7.801l47.243 33.282C253.406 47.414 256 48.975 256 55.207v182.527c0 11.439-4.155 18.205-18.696 19.24L65.44 267.378c-10.913.517-16.11-1.043-21.825-8.327L8.826 213.814C2.586 205.487 0 199.254 0 191.97V29.726c0-9.352 4.155-17.153 16.092-18.188Z"
    />
    <path d="M164.09.608 16.092 11.538C4.155 12.573 0 20.374 0 29.726v162.245c0 7.284 2.585 13.516 8.826 21.843l34.789 45.237c5.715 7.284 10.912 8.844 21.825 8.327l171.864-10.404c14.532-1.035 18.696-7.801 18.696-19.24V55.207c0-5.911-2.336-7.614-9.21-12.66l-1.185-.856L198.37 8.409C186.94.1 182.27-.952 164.09.608ZM69.327 52.22c-14.033.945-17.216 1.159-25.186-5.323L23.876 30.778c-2.06-2.086-1.026-4.69 4.163-5.207l142.274-10.395c11.947-1.043 18.17 3.12 22.842 6.758l24.401 17.68c1.043.525 3.638 3.637.517 3.637L71.146 52.095l-1.819.125Zm-16.36 183.954V81.222c0-6.767 2.077-9.887 8.3-10.413L230.02 60.93c5.724-.517 8.31 3.12 8.31 9.879v153.917c0 6.767-1.044 12.49-10.387 13.008l-161.487 9.361c-9.343.517-13.489-2.594-13.489-10.921ZM212.377 89.53c1.034 4.681 0 9.362-4.681 9.897l-7.783 1.542v114.404c-6.758 3.637-12.981 5.715-18.18 5.715-8.308 0-10.386-2.604-16.609-10.396l-50.898-80.079v77.476l16.1 3.646s0 9.362-12.989 9.362l-35.814 2.077c-1.043-2.086 0-7.284 3.63-8.318l9.351-2.595V109.823l-12.98-1.052c-1.044-4.68 1.55-11.439 8.826-11.965l38.426-2.585 52.958 81.113v-71.76l-13.498-1.552c-1.043-5.733 3.111-9.896 8.3-10.404l35.84-2.087Z" />
  </svg>
);

export const GoogleLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 268.152 273.883" aria-hidden="true" {...props}>
    <defs>
      <linearGradient id="google-a">
        <stop offset="0" stopColor="#0fbc5c" />
        <stop offset="1" stopColor="#0cba65" />
      </linearGradient>
      <linearGradient id="google-g">
        <stop offset=".231" stopColor="#0fbc5f" />
        <stop offset=".312" stopColor="#0fbc5f" />
        <stop offset=".366" stopColor="#0fbc5e" />
        <stop offset=".458" stopColor="#0fbc5d" />
        <stop offset=".54" stopColor="#12bc58" />
        <stop offset=".699" stopColor="#28bf3c" />
        <stop offset=".771" stopColor="#38c02b" />
        <stop offset=".861" stopColor="#52c218" />
        <stop offset=".915" stopColor="#67c30f" />
        <stop offset="1" stopColor="#86c504" />
      </linearGradient>
      <linearGradient id="google-h">
        <stop offset=".142" stopColor="#1abd4d" />
        <stop offset=".248" stopColor="#6ec30d" />
        <stop offset=".312" stopColor="#8ac502" />
        <stop offset=".366" stopColor="#a2c600" />
        <stop offset=".446" stopColor="#c8c903" />
        <stop offset=".54" stopColor="#ebcb03" />
        <stop offset=".616" stopColor="#f7cd07" />
        <stop offset=".699" stopColor="#fdcd04" />
        <stop offset=".771" stopColor="#fdce05" />
        <stop offset=".861" stopColor="#ffce0a" />
      </linearGradient>
      <linearGradient id="google-f">
        <stop offset=".316" stopColor="#ff4c3c" />
        <stop offset=".604" stopColor="#ff692c" />
        <stop offset=".727" stopColor="#ff7825" />
        <stop offset=".885" stopColor="#ff8d1b" />
        <stop offset="1" stopColor="#ff9f13" />
      </linearGradient>
      <linearGradient id="google-b">
        <stop offset=".231" stopColor="#ff4541" />
        <stop offset=".312" stopColor="#ff4540" />
        <stop offset=".458" stopColor="#ff4640" />
        <stop offset=".54" stopColor="#ff473f" />
        <stop offset=".699" stopColor="#ff5138" />
        <stop offset=".771" stopColor="#ff5b33" />
        <stop offset=".861" stopColor="#ff6c29" />
        <stop offset="1" stopColor="#ff8c18" />
      </linearGradient>
      <linearGradient id="google-d">
        <stop offset=".408" stopColor="#fb4e5a" />
        <stop offset="1" stopColor="#ff4540" />
      </linearGradient>
      <linearGradient id="google-c">
        <stop offset=".132" stopColor="#0cba65" />
        <stop offset=".21" stopColor="#0bb86d" />
        <stop offset=".297" stopColor="#09b479" />
        <stop offset=".396" stopColor="#08ad93" />
        <stop offset=".477" stopColor="#0aa6a9" />
        <stop offset=".568" stopColor="#0d9cc6" />
        <stop offset=".667" stopColor="#1893dd" />
        <stop offset=".769" stopColor="#258bf1" />
        <stop offset=".859" stopColor="#3086ff" />
      </linearGradient>
      <linearGradient id="google-e">
        <stop offset=".366" stopColor="#ff4e3a" />
        <stop offset=".458" stopColor="#ff8a1b" />
        <stop offset=".54" stopColor="#ffa312" />
        <stop offset=".616" stopColor="#ffb60c" />
        <stop offset=".771" stopColor="#ffcd0a" />
        <stop offset=".861" stopColor="#fecf0a" />
        <stop offset=".915" stopColor="#fecf08" />
        <stop offset="1" stopColor="#fdcd01" />
      </linearGradient>
      <linearGradient
        href="#google-a"
        id="google-s"
        x1="219.7"
        x2="254.467"
        y1="329.535"
        y2="329.535"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-b"
        id="google-m"
        cx="109.627"
        cy="135.862"
        r="71.46"
        fx="109.627"
        fy="135.862"
        gradientTransform="matrix(-1.93688 1.043 1.45573 2.55542 290.525 -400.634)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-c"
        id="google-n"
        cx="45.259"
        cy="279.274"
        r="71.46"
        fx="45.259"
        fy="279.274"
        gradientTransform="matrix(-3.5126 -4.45809 -1.69255 1.26062 870.8 191.554)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-d"
        id="google-l"
        cx="304.017"
        cy="118.009"
        r="47.854"
        fx="304.017"
        fy="118.009"
        gradientTransform="matrix(2.06435 0 0 2.59204 -297.679 -151.747)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-e"
        id="google-o"
        cx="181.001"
        cy="177.201"
        r="71.46"
        fx="181.001"
        fy="177.201"
        gradientTransform="matrix(-.24858 2.08314 2.96249 .33417 -255.146 -331.164)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-f"
        id="google-p"
        cx="207.673"
        cy="108.097"
        r="41.102"
        fx="207.673"
        fy="108.097"
        gradientTransform="matrix(-1.2492 1.34326 -3.89684 -3.4257 880.501 194.905)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-g"
        id="google-r"
        cx="109.627"
        cy="135.862"
        r="71.46"
        fx="109.627"
        fy="135.862"
        gradientTransform="matrix(-1.93688 -1.043 1.45573 -2.55542 290.525 838.683)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-h"
        id="google-j"
        cx="154.87"
        cy="145.969"
        r="71.46"
        fx="154.87"
        fy="145.969"
        gradientTransform="matrix(-.0814 -1.93722 2.92674 -.11625 -215.135 632.86)"
        gradientUnits="userSpaceOnUse"
      />
      <filter
        id="google-q"
        width="1.097"
        height="1.116"
        x="-.048"
        y="-.058"
        colorInterpolationFilters="sRGB"
      >
        <feGaussianBlur stdDeviation="1.701" />
      </filter>
      <filter
        id="google-k"
        width="1.033"
        height="1.02"
        x="-.017"
        y="-.01"
        colorInterpolationFilters="sRGB"
      >
        <feGaussianBlur stdDeviation=".242" />
      </filter>
      <clipPath id="google-i" clipPathUnits="userSpaceOnUse">
        <path d="M371.378 193.24H237.083v53.438h77.167c-1.241 7.563-4.026 15.003-8.105 21.786-4.674 7.773-10.451 13.69-16.373 18.196-17.74 13.498-38.42 16.258-52.783 16.258-36.283 0-67.283-23.286-79.285-54.928-.484-1.149-.805-2.335-1.197-3.507a81.115 81.115 0 0 1-4.101-25.448c0-9.226 1.569-18.057 4.43-26.398 11.285-32.897 42.985-57.467 80.179-57.467 7.481 0 14.685.884 21.517 2.648a77.668 77.668 0 0 1 33.425 18.25l40.834-39.712c-24.839-22.616-57.219-36.32-95.844-36.32-30.878 0-59.386 9.553-82.748 25.7-18.945 13.093-34.483 30.625-44.97 50.985-9.753 18.879-15.094 39.8-15.094 62.294 0 22.495 5.35 43.633 15.103 62.337v.126c10.302 19.857 25.368 36.954 43.678 49.988 15.997 11.386 44.68 26.551 84.031 26.551 22.63 0 42.687-4.051 60.375-11.644 12.76-5.478 24.065-12.622 34.301-21.804 13.525-12.132 24.117-27.139 31.347-44.404 7.23-17.265 11.097-36.79 11.097-57.957 0-9.858-.998-19.87-2.689-28.968Z" />
      </clipPath>
    </defs>
    <g
      clipPath="url(#google-i)"
      transform="matrix(.95792 0 0 .98525 -90.174 -78.856)"
    >
      <path
        fill="url(#google-j)"
        d="M92.076 219.958c.148 22.14 6.501 44.983 16.117 63.424v.127c6.949 13.392 16.445 23.97 27.26 34.452l65.327-23.67c-12.36-6.235-14.246-10.055-23.105-17.026-9.054-9.066-15.802-19.473-20.004-31.677h-.17l.17-.127c-2.765-8.058-3.037-16.613-3.14-25.503Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-l)"
        d="M237.083 79.025c-6.456 22.526-3.988 44.421 0 57.161 7.457.006 14.64.888 21.45 2.647a77.662 77.662 0 0 1 33.424 18.25l41.88-40.726c-24.81-22.59-54.667-37.297-96.754-37.332Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-m)"
        d="M236.943 78.847c-31.67 0-60.91 9.798-84.871 26.359a145.533 145.533 0 0 0-24.332 21.15c-1.904 17.744 14.257 39.551 46.262 39.37 15.528-17.936 38.495-29.542 64.056-29.542l.07.002-1.044-57.335c-.048 0-.093-.004-.14-.004Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-n)"
        d="m341.475 226.379-28.268 19.285c-1.24 7.562-4.028 15.002-8.107 21.786-4.674 7.772-10.45 13.69-16.373 18.196-17.702 13.47-38.328 16.244-52.687 16.255-14.842 25.102-17.444 37.675 1.043 57.934 22.877-.016 43.157-4.117 61.046-11.796 12.931-5.551 24.388-12.792 34.761-22.097 13.706-12.295 24.442-27.503 31.769-45 7.327-17.497 11.245-37.282 11.245-58.734Z"
        filter="url(#google-k)"
      />
      <path
        fill="#3086ff"
        d="M234.996 191.21v57.498h136.006c1.196-7.874 5.152-18.064 5.152-26.5 0-9.858-.996-21.899-2.687-30.998Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-o)"
        d="M128.39 124.327c-8.394 9.119-15.564 19.326-21.249 30.364-9.753 18.879-15.094 41.83-15.094 64.324 0 .317.026.627.029.944 4.32 8.224 59.666 6.649 62.456 0-.004-.31-.039-.613-.039-.924 0-9.226 1.57-16.026 4.43-24.367 3.53-10.289 9.056-19.763 16.123-27.926 1.602-2.031 5.875-6.397 7.121-9.016.475-.997-.862-1.557-.937-1.908-.083-.393-1.876-.077-2.277-.37-1.275-.929-3.8-1.414-5.334-1.845-3.277-.921-8.708-2.953-11.725-5.06-9.536-6.658-24.417-14.612-33.505-24.216Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-p)"
        d="M162.099 155.857c22.112 13.301 28.471-6.714 43.173-12.977l-25.574-52.664a144.74 144.74 0 0 0-26.543 14.504c-12.316 8.512-23.192 18.9-32.176 30.72Z"
        filter="url(#google-q)"
      />
      <path
        fill="url(#google-r)"
        d="M171.099 290.222c-29.683 10.641-34.33 11.023-37.062 29.29a144.806 144.806 0 0 0 16.792 13.984c15.996 11.386 46.766 26.551 86.118 26.551.046 0 .09-.004.137-.004v-59.157l-.094.002c-14.736 0-26.512-3.843-38.585-10.527-2.977-1.648-8.378 2.777-11.123.799-3.786-2.729-12.9 2.35-16.183-.938Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-s)"
        d="M219.7 299.023v59.996c5.506.64 11.236 1.028 17.247 1.028 6.026 0 11.855-.307 17.52-.872v-59.748a105.119 105.119 0 0 1-17.477 1.461c-5.932 0-11.7-.686-17.29-1.865Z"
        filter="url(#google-k)"
        opacity=".5"
      />
    </g>
  </svg>
);

export const GoogleSheetsLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 74 100" fill="none" aria-hidden="true" {...props}>
    <mask
      id="google-sheets-mask0_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask0_1-52)">
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L56.4365 16.8843L45.398 1.43036Z"
        fill="#0F9D58"
      />
    </g>
    <mask
      id="google-sheets-mask1_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask1_1-52)">
      <path
        d="M18.9054 48.8962V80.908H54.2288V48.8962H18.9054ZM34.3594 76.4926H23.3209V70.9733H34.3594V76.4926ZM34.3594 67.6617H23.3209V62.1424H34.3594V67.6617ZM34.3594 58.8309H23.3209V53.3116H34.3594V58.8309ZM49.8134 76.4926H38.7748V70.9733H49.8134V76.4926ZM49.8134 67.6617H38.7748V62.1424H49.8134V67.6617ZM49.8134 58.8309H38.7748V53.3116H49.8134V58.8309Z"
        fill="#F1F1F1"
      />
    </g>
    <mask
      id="google-sheets-mask2_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask2_1-52)">
      <path
        d="M47.3352 25.9856L71.8905 50.5354V27.9229L47.3352 25.9856Z"
        fill="url(#google-sheets-paint0_linear_1-52)"
      />
    </g>
    <mask
      id="google-sheets-mask3_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask3_1-52)">
      <path
        d="M45.398 1.43036V21.2998C45.398 24.959 48.3618 27.9229 52.0211 27.9229H71.8905L45.398 1.43036Z"
        fill="#87CEAC"
      />
    </g>
    <mask
      id="google-sheets-mask4_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask4_1-52)">
      <path
        d="M7.86688 1.43036C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V8.60542C1.24374 4.9627 4.22415 1.98229 7.86688 1.98229H45.398V1.43036H7.86688Z"
        fill="white"
        fillOpacity="0.2"
      />
    </g>
    <mask
      id="google-sheets-mask5_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask5_1-52)">
      <path
        d="M65.2674 98.0177H7.86688C4.22415 98.0177 1.24374 95.0373 1.24374 91.3946V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V91.3946C71.8905 95.0373 68.9101 98.0177 65.2674 98.0177Z"
        fill="#263238"
        fillOpacity="0.2"
      />
    </g>
    <mask
      id="google-sheets-mask6_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask6_1-52)">
      <path
        d="M52.0211 27.9229C48.3618 27.9229 45.398 24.959 45.398 21.2998V21.8517C45.398 25.511 48.3618 28.4748 52.0211 28.4748H71.8905V27.9229H52.0211Z"
        fill="#263238"
        fillOpacity="0.1"
      />
    </g>
    <path
      d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
      fill="url(#google-sheets-paint1_radial_1-52)"
    />
    <defs>
      <linearGradient
        id="google-sheets-paint0_linear_1-52"
        x1="59.6142"
        y1="28.0935"
        x2="59.6142"
        y2="50.5388"
        gradientUnits="userSpaceOnUse"
      >
        <stop stopColor="#263238" stopOpacity="0.2" />
        <stop offset="1" stopColor="#263238" stopOpacity="0.02" />
      </linearGradient>
      <radialGradient
        id="google-sheets-paint1_radial_1-52"
        cx="0"
        cy="0"
        r="1"
        gradientUnits="userSpaceOnUse"
        gradientTransform="translate(3.48187 3.36121) scale(113.917)"
      >
        <stop stopColor="white" stopOpacity="0.1" />
        <stop offset="1" stopColor="white" stopOpacity="0" />
      </radialGradient>
    </defs>
  </svg>
);

export const WebflowLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 1080 674" fill="none" aria-hidden="true" {...props}>
    <path
      fill="#146EF5"
      fillRule="evenodd"
      d="M1080 0 735.386 673.684H411.695l144.221-279.203h-6.471C430.464 548.934 252.942 650.61-.0005 673.684v-275.34s161.8135-9.557 256.9385-109.568H-.0005V.0053H288.771V237.515l6.481-.026L413.254.0053h218.39V236.009l6.482-.01L760.555 0H1080Z"
      clipRule="evenodd"
    />
  </svg>
);

export const WordPressLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 122.52 122.523" aria-hidden="true" {...props}>
    <g fill="#21759b">
      <path d="m8.708 61.26c0 20.802 12.089 38.779 29.619 47.298l-25.069-68.686c-2.916 6.536-4.55 13.769-4.55 21.388z" />
      <path d="m96.74 58.608c0-6.495-2.333-10.993-4.334-14.494-2.664-4.329-5.161-7.995-5.161-12.324 0-4.831 3.664-9.328 8.825-9.328.233 0 .454.029.681.042-9.35-8.566-21.807-13.796-35.489-13.796-18.36 0-34.513 9.42-43.91 23.688 1.233.037 2.395.063 3.382.063 5.497 0 14.006-.667 14.006-.667 2.833-.167 3.167 3.994.337 4.329 0 0-2.847.335-6.015.501l19.138 56.925 11.501-34.493-8.188-22.434c-2.83-.166-5.511-.501-5.511-.501-2.832-.166-2.5-4.496.332-4.329 0 0 8.679.667 13.843.667 5.496 0 14.006-.667 14.006-.667 2.835-.167 3.168 3.994.337 4.329 0 0-2.853.335-6.015.501l18.992 56.494 5.242-17.517c2.272-7.269 4.001-12.49 4.001-16.989z" />
      <path d="m62.184 65.857-15.768 45.819c4.708 1.384 9.687 2.141 14.846 2.141 6.12 0 11.989-1.058 17.452-2.979-.141-.225-.269-.464-.374-.724z" />
      <path d="m107.376 36.046c.226 1.674.354 3.471.354 5.404 0 5.333-.996 11.328-3.996 18.824l-16.053 46.413c15.624-9.111 26.133-26.038 26.133-45.426.001-9.137-2.333-17.729-6.438-25.215z" />
      <path d="m61.262 0c-33.779 0-61.262 27.481-61.262 61.26 0 33.783 27.483 61.263 61.262 61.263 33.778 0 61.265-27.48 61.265-61.263-.001-33.779-27.487-61.26-61.265-61.26zm0 119.715c-32.23 0-58.453-26.223-58.453-58.455 0-32.23 26.222-58.451 58.453-58.451 32.229 0 58.45 26.221 58.45 58.451 0 32.232-26.221 58.455-58.45 58.455z" />
    </g>
  </svg>
);

export const LinearLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 100 100" fill="none" aria-hidden="true" {...props}>
    <path
      fill="#5E6AD2"
      d="M1.225 61.523c-.222-.949.908-1.546 1.597-.857l36.512 36.512c.69.69.092 1.82-.857 1.597-18.425-4.323-32.93-18.827-37.252-37.252ZM.002 46.889a.99.99 0 0 0 .29.76L52.35 99.71c.201.2.478.307.76.29 2.37-.149 4.695-.46 6.963-.927.765-.157 1.03-1.096.478-1.648L2.576 39.448c-.552-.551-1.491-.286-1.648.479a50.067 50.067 0 0 0-.926 6.962ZM4.21 29.705a.988.988 0 0 0 .208 1.1l64.776 64.776c.289.29.726.375 1.1.208a49.908 49.908 0 0 0 5.185-2.684.981.981 0 0 0 .183-1.54L8.436 24.336a.981.981 0 0 0-1.541.183 49.896 49.896 0 0 0-2.684 5.185Zm8.448-11.631a.986.986 0 0 1-.045-1.354C21.78 6.46 35.111 0 49.952 0 77.592 0 100 22.407 100 50.048c0 14.84-6.46 28.172-16.72 37.338a.986.986 0 0 1-1.354-.045L12.659 18.074Z"
    />
  </svg>
);

export const GitHubLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg
    viewBox="0 0 1024 1024"
    fill="currentColor"
    aria-hidden="true"
    {...props}
  >
    <path
      fillRule="evenodd"
      d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28s87.04 5.76 128 17.28c97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0"
      clipRule="evenodd"
    />
  </svg>
);

export const AhrefsLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="16 8 96 112" fill="none" aria-hidden="true" {...props}>
    <path
      d="m24 8v24h64v16l-26.104 0.456c-35.096 3.256-45.896 11.624-45.896 36.504v6.512c0 19.064 14.328 28.528 32 28.528 7.984 0 16.32-1.56 25.04-5.976l14.96-6.024v12h24v-112zm64 76-18.112 7.472c-6.296 3.488-12.832 5.344-17.92 5.344-6.288 0-11.968-1.136-11.968-8.816v-5.12c0.48-8.136 7.376-11.176 26.256-13.032l21.744-1.848z"
      fill="#f80"
    />
  </svg>
);

export const PostHogLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 2.41 47.99 26.58" fill="none" aria-hidden="true" {...props}>
    <path
      fill="#1D4AFF"
      d="M10.891 17.206a1 1 0 0 1-1.788 0l-.882-1.763a1 1 0 0 1 0-.894l.882-1.763a1 1 0 0 1 1.788 0l.882 1.763a1 1 0 0 1 0 .894l-.882 1.763zm0 9.997a1 1 0 0 1-1.788 0L8.22 25.44a1 1 0 0 1 0-.894l.882-1.763a1 1 0 0 1 1.788 0l.882 1.763a1 1 0 0 1 0 .894l-.882 1.763z"
    />
    <path
      fill="#F9BD2B"
      d="M0 23.408c0-.89 1.077-1.337 1.707-.707l4.583 4.583c.63.63.184 1.708-.707 1.708H1a1 1 0 0 1-1-1v-4.584zm0-4.828a1 1 0 0 0 .293.708l9.411 9.41a1 1 0 0 0 .707.294h5.17c.89 0 1.337-1.077.707-1.707l-14.58-14.58C1.077 12.074 0 12.52 0 13.41v5.17zm0-9.997a1 1 0 0 0 .293.707L19.7 28.7a1 1 0 0 0 .707.293h5.17c.89 0 1.337-1.078.707-1.708L1.707 2.707C1.077 2.077 0 2.523 0 3.414v5.17zm9.997 0a1 1 0 0 0 .293.707l17.994 17.995c.63.63 1.707.183 1.707-.708v-5.169a1 1 0 0 0-.293-.707L11.704 2.707c-.63-.63-1.707-.184-1.707.707v5.17zm11.704-5.876c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l7.997 7.998c.63.63 1.707.183 1.707-.708v-5.169a1 1 0 0 0-.293-.707l-7.997-7.997z"
    />
    <path
      fill="#000"
      d="m42.525 23.53-9.413-9.412c-.63-.63-1.707-.184-1.707.707v13.167a1 1 0 0 0 1 1h14.58a1 1 0 0 0 1-1v-1.2c0-.552-.449-.993-.997-1.064a7.723 7.723 0 0 1-4.463-2.197zm-6.321 2.263a1.6 1.6 0 1 1 0-3.2 1.6 1.6 0 0 1 0 3.2z"
    />
    <path
      fill="#1D4AFF"
      d="M0 27.992a1 1 0 0 0 1 1h4.583c.891 0 1.337-1.078.707-1.708l-4.583-4.583c-.63-.63-1.707-.184-1.707.707v4.584zm9.997-16.995-8.29-8.29C1.077 2.077 0 2.523 0 3.414v5.17a1 1 0 0 0 .293.706l9.704 9.705v-7.998zm-8.29 1.707c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l9.704 9.705v-7.998l-8.29-8.29z"
    />
    <path
      fill="#F54E00"
      d="M19.994 11.411a1 1 0 0 0-.293-.707l-7.997-7.997c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l9.704 9.705V11.41zm-9.997 17.58h5.583c.891 0 1.337-1.077.707-1.707l-6.29-6.29v7.998zm0-17.994v7.583a1 1 0 0 0 .293.708l9.704 9.704v-7.584a1 1 0 0 0-.293-.707l-9.704-9.704z"
    />
  </svg>
);
```

### `registry/notra/workflow/components/workflow-shell.tsx`

```tsx
import { cn } from "cn";
import type { ComponentProps } from "react";

export const WorkflowShell = ({
  className,
  ...props
}: ComponentProps<"div">) => (
  <div
    className={cn(
      "border-table-shell-border bg-table-shell flex min-w-0 flex-col rounded-2xl border p-0.5",
      className
    )}
    data-slot="workflow-shell"
    {...props}
  />
);

export const WorkflowShellCard = ({
  className,
  ...props
}: ComponentProps<"div">) => (
  <div
    className={cn(
      "border-table-card-border bg-table-card shadow-table-card relative min-w-0 rounded-[14px] border",
      className
    )}
    data-slot="workflow-shell-card"
    {...props}
  />
);

export const WorkflowShellHeader = ({
  className,
  ...props
}: ComponentProps<"div">) => (
  <div
    className={cn(
      "flex min-h-10 min-w-0 flex-wrap items-center gap-2 px-3 py-1.5 text-sm font-medium",
      className
    )}
    data-slot="workflow-shell-header"
    {...props}
  />
);
```

### `registry/notra/workflow/constants/workflow-ui.ts`

```ts
import type {
  WorkflowBannerConfig,
  WorkflowEdgeStatus,
  WorkflowRunPhase,
} from "../types/workflow";

export const WORKFLOW_NODE_WIDTH = 256;

export const WORKFLOW_NODE_DROP_OFFSET_Y = 24;

export const WORKFLOW_COLUMN = 320;

export const WORKFLOW_FIT_PADDING = 0.12;

export const WORKFLOW_FIT_MIN_ZOOM = 0.6;

export const WORKFLOW_FIT_MAX_ZOOM = 1;

export const WORKFLOW_FIT_EDGE_PX = 32;

export const WORKFLOW_FIT_VIEW_OPTIONS = {
  maxZoom: WORKFLOW_FIT_MAX_ZOOM,
  padding: WORKFLOW_FIT_PADDING,
};

export const WORKFLOW_ZOOM_DURATION_MS = 200;

export const WORKFLOW_FIT_DURATION_MS = 300;

export const WORKFLOW_MINIMAP_STYLE = { height: 96, width: 144 };

export const WORKFLOW_PATTERN_STRENGTH = {
  cross: 22,
  dots: 24,
  gridMajor: 12,
  gridMinor: 5,
  lines: 7,
};

export const WORKFLOW_PORT_ROW_CLASS =
  "text-muted-foreground border-table-card-border relative flex h-7.5 min-w-0 items-center border-t px-3 text-[0.6875rem] font-medium";

export const WORKFLOW_CHIP_CLASS =
  "bg-foreground/[0.05] text-muted-foreground inline-flex h-5 min-w-0 items-center gap-1 rounded-full px-2 text-[0.6875rem] leading-none tabular-nums [&_svg]:size-3 [&_svg]:shrink-0";

export const WORKFLOW_BANNERS: Record<
  "error" | "waiting",
  WorkflowBannerConfig
> = {
  error: {
    className: "text-red-600 dark:text-red-400",
    label: "Failed",
    message: "Check the run log",
  },
  waiting: {
    className: "text-amber-600 dark:text-amber-400",
    label: "Waiting",
    message: "Needs approval",
  },
};

export const WORKFLOW_PILL_CLASS =
  "bg-table-shell border-table-shell-border text-muted-foreground inline-flex h-9 w-7 shrink-0 flex-col overflow-hidden rounded-lg border [&_svg]:size-3";

export const WORKFLOW_PILL_TOP_CLASS =
  "flex flex-1 items-center justify-center";

export const WORKFLOW_PILL_BOTTOM_CLASS =
  "border-table-shell-border flex flex-1 items-center justify-center border-t";

export const WORKFLOW_LED_CLASS =
  "ring-background/60 size-1.5 rounded-full ring-2 transition-colors duration-300";

export const WORKFLOW_EDGE_COLOR: Record<WorkflowEdgeStatus, string> = {
  active: "text-primary",
  done: "text-primary/70",
  idle: "text-muted-foreground/70",
  skipped: "text-muted-foreground/25",
};

export const WORKFLOW_EDGE_DASH = "0 10";

export const WORKFLOW_EDGE_STEP_OPTIONS = { borderRadius: 24, offset: 24 };

export const WORKFLOW_EDGE_RADIUS = 24;

export const WORKFLOW_RUN_LABELS: Record<WorkflowRunPhase, string> = {
  done: "Run again",
  idle: "Run workflow",
  running: "Stop",
};

export const WORKFLOW_RUN_RING_RADIUS = 10;

export const WORKFLOW_RUN_RING_LENGTH = 2 * Math.PI * WORKFLOW_RUN_RING_RADIUS;

export const WORKFLOW_SECTION_TITLE_CLASS =
  "text-muted-foreground text-[0.6875rem] font-medium tracking-wide uppercase";

export const WORKFLOW_EDGE_REVERSE_STYLE = { animationDirection: "reverse" };

export const WORKFLOW_SKIPPED_DIM_CLASS =
  "transition-opacity duration-300 group-data-[status=skipped]/node:opacity-45";

export const WORKFLOW_BRANCH_RAIL_CLASS =
  "border-table-card-border bg-table-card/60 divide-table-card-border mt-0.5 divide-y rounded-[14px] border";

export const WORKFLOW_BRANCH_ROW_CLASS =
  "relative flex h-10 min-w-0 items-center gap-2.5 px-3";

export const WORKFLOW_BRANCH_LED_CLASS =
  "size-2 shrink-0 rounded-full ring-2 transition-[background-color,box-shadow,opacity] duration-300";

export const WORKFLOW_RULE_TOKEN_CLASS =
  "bg-foreground/[0.05] text-foreground/80 inline-flex h-5 max-w-full min-w-0 items-center truncate rounded-md px-1.5 text-[0.6875rem] whitespace-nowrap";
```

### `registry/notra/workflow/constants/workflow.ts`

```ts
import {
  FlagIcon,
  GitForkIcon,
  HourglassIcon,
  MailIcon,
  MergeIcon,
  PlugIcon,
  RepeatIcon,
  SparklesIcon,
  SplitIcon,
  SquareFunctionIcon,
  UserCheckIcon,
  ZapIcon,
} from "lucide-react";

import type {
  WorkflowKindConfig,
  WorkflowModel,
  WorkflowNodeKind,
  WorkflowNodeStatus,
  WorkflowPort,
  WorkflowStatusConfig,
} from "../types/workflow";

export const WORKFLOW_DEFAULT_DURATION_MS = 900;

export const WORKFLOW_EDGE_TRAVEL_MS = 450;

export const WORKFLOW_TICK_MS = 50;

export const WORKFLOW_BLOCK_MIME = "application/x-workflow-block";

export const WORKFLOW_IN_PORT: WorkflowPort = { id: "in" };

export const WORKFLOW_OUT_PORT: WorkflowPort = { id: "out" };

export const WORKFLOW_KINDS: Record<WorkflowNodeKind, WorkflowKindConfig> = {
  ai: {
    icon: SparklesIcon,
    label: "AI step",
    tileClassName:
      "bg-violet-500/10 text-violet-600 ring-violet-500/20 dark:text-violet-300",
  },
  approval: {
    icon: UserCheckIcon,
    label: "Human approval",
    tileClassName:
      "bg-pink-500/10 text-pink-600 ring-pink-500/20 dark:text-pink-300",
  },
  condition: {
    icon: SplitIcon,
    label: "If / else",
    tileClassName:
      "bg-orange-500/10 text-orange-600 ring-orange-500/20 dark:text-orange-300",
  },
  delay: {
    icon: HourglassIcon,
    label: "Wait",
    tileClassName:
      "bg-zinc-500/10 text-zinc-600 ring-zinc-500/20 dark:text-zinc-300",
  },
  email: {
    icon: MailIcon,
    label: "Email",
    tileClassName:
      "bg-emerald-500/10 text-emerald-600 ring-emerald-500/20 dark:text-emerald-300",
  },
  integration: {
    icon: PlugIcon,
    label: "Integration",
    tileClassName:
      "bg-sky-500/10 text-sky-600 ring-sky-500/20 dark:text-sky-300",
  },
  loop: {
    icon: RepeatIcon,
    label: "For each",
    tileClassName:
      "bg-indigo-500/10 text-indigo-600 ring-indigo-500/20 dark:text-indigo-300",
  },
  merge: {
    icon: MergeIcon,
    label: "Merge",
    tileClassName:
      "bg-teal-500/10 text-teal-600 ring-teal-500/20 dark:text-teal-300",
  },
  output: {
    icon: FlagIcon,
    label: "Output",
    tileClassName:
      "bg-zinc-500/10 text-zinc-600 ring-zinc-500/20 dark:text-zinc-300",
  },
  switch: {
    icon: GitForkIcon,
    label: "Decision",
    tileClassName:
      "bg-orange-500/10 text-orange-600 ring-orange-500/20 dark:text-orange-300",
  },
  task: {
    icon: SquareFunctionIcon,
    label: "Task",
    tileClassName:
      "bg-blue-500/10 text-blue-600 ring-blue-500/20 dark:text-blue-300",
  },
  trigger: {
    icon: ZapIcon,
    label: "Trigger",
    tileClassName:
      "bg-amber-500/10 text-amber-600 ring-amber-500/20 dark:text-amber-300",
  },
};

export const WORKFLOW_NON_DETERMINISTIC_KINDS: WorkflowNodeKind[] = ["ai"];

export const WORKFLOW_STATUSES: Record<
  WorkflowNodeStatus,
  WorkflowStatusConfig
> = {
  error: {
    className: "text-red-600 dark:text-red-400",
    label: "Failed",
    ledClassName: "bg-red-500 shadow-[0_0_6px_var(--color-red-500)]",
  },
  idle: {
    className: "text-muted-foreground",
    label: "Idle",
    ledClassName: "bg-muted-foreground/35",
  },
  queued: {
    className: "text-muted-foreground",
    label: "Queued",
    ledClassName: "bg-muted-foreground/35",
  },
  running: {
    className: "text-primary",
    label: "Running",
    ledClassName:
      "bg-primary shadow-[0_0_6px_var(--color-primary,var(--primary))] motion-safe:animate-pulse",
  },
  skipped: {
    className: "text-muted-foreground",
    label: "Skipped",
    ledClassName: "bg-muted-foreground/20",
  },
  success: {
    className: "text-emerald-600 dark:text-emerald-400",
    label: "Completed",
    ledClassName: "bg-emerald-500 shadow-[0_0_6px_var(--color-emerald-500)]",
  },
  waiting: {
    className: "text-amber-600 dark:text-amber-400",
    label: "Waiting for approval",
    ledClassName:
      "bg-amber-500 shadow-[0_0_6px_var(--color-amber-500)] motion-safe:animate-pulse",
  },
};

export const WORKFLOW_NODE_RING: Record<WorkflowNodeStatus, string> = {
  error: "border-red-500/60",
  idle: "",
  queued: "",
  running:
    "border-primary shadow-[0_0_0_0.3rem_color-mix(in_oklab,var(--color-primary,var(--primary))_14%,transparent)]",
  skipped: "",
  success: "border-emerald-500/50",
  waiting:
    "border-amber-500 shadow-[0_0_0_0.3rem_color-mix(in_oklab,var(--color-amber-500)_14%,transparent)]",
};

export const WORKFLOW_MODELS: WorkflowModel[] = [
  {
    id: "claude-opus-5-5",
    label: "Claude Opus 5.5",
    logo: "claude",
    provider: "Anthropic",
  },
  {
    id: "claude-sonnet-5-5",
    label: "Claude Sonnet 5.5",
    logo: "claude",
    provider: "Anthropic",
  },
  {
    id: "claude-haiku-5-5",
    label: "Claude Haiku 5.5",
    logo: "claude",
    provider: "Anthropic",
  },
  { id: "gpt-5", label: "GPT-5", logo: "openai", provider: "OpenAI" },
  { id: "gpt-5-mini", label: "GPT-5 mini", logo: "openai", provider: "OpenAI" },
  {
    id: "gemini-2-5-pro",
    label: "Gemini 2.5 Pro",
    logo: "gemini",
    provider: "Google",
  },
  {
    id: "sonar-pro",
    label: "Sonar Pro",
    logo: "perplexity",
    provider: "Perplexity",
  },
  { id: "grok-4", label: "Grok 4", logo: "grok", provider: "xAI" },
  {
    id: "mistral-large",
    label: "Mistral Large",
    logo: "mistral",
    provider: "Mistral",
  },
  {
    id: "deepseek-v3",
    label: "DeepSeek V3",
    logo: "deepseek",
    provider: "DeepSeek",
  },
];
```

### `registry/notra/workflow/lib/workflow.ts`

```ts
import { getSmoothStepPath } from "@xyflow/react";

import {
  WORKFLOW_DEFAULT_DURATION_MS,
  WORKFLOW_EDGE_TRAVEL_MS,
  WORKFLOW_IN_PORT,
  WORKFLOW_MODELS,
  WORKFLOW_NON_DETERMINISTIC_KINDS,
  WORKFLOW_OUT_PORT,
} from "../constants/workflow";
import {
  WORKFLOW_EDGE_RADIUS,
  WORKFLOW_EDGE_STEP_OPTIONS,
} from "../constants/workflow-ui";
import type {
  WorkflowAnchor,
  WorkflowBlock,
  WorkflowEdgeData,
  WorkflowEdgeStatus,
  WorkflowLogEntry,
  WorkflowNodeData,
  WorkflowNodeState,
  WorkflowPoint,
  WorkflowPort,
  WorkflowRunPhase,
  WorkflowSchedule,
} from "../types/workflow";

export const getInputPorts = (node: WorkflowNodeData): WorkflowPort[] =>
  node.inputs ?? (node.kind === "trigger" ? [] : [WORKFLOW_IN_PORT]);

export const getOutputPorts = (node: WorkflowNodeData): WorkflowPort[] =>
  node.outputs ?? (node.kind === "output" ? [] : [WORKFLOW_OUT_PORT]);

export const isDeterministic = (node: Pick<WorkflowNodeData, "kind">) =>
  !WORKFLOW_NON_DETERMINISTIC_KINDS.includes(node.kind);

export const findModel = (id?: string) =>
  WORKFLOW_MODELS.find((model) => model.id === id);

const byPosition = (a: WorkflowNodeData, b: WorkflowNodeData) =>
  a.position.x - b.position.x || a.position.y - b.position.y;

export const getRunOrder = (
  nodes: WorkflowNodeData[],
  edges: WorkflowEdgeData[]
) => {
  const indegree = new Map(nodes.map((node) => [node.id, 0]));
  for (const edge of edges) {
    if (indegree.has(edge.target) && indegree.has(edge.source)) {
      indegree.set(edge.target, (indegree.get(edge.target) ?? 0) + 1);
    }
  }

  const byId = new Map(nodes.map((node) => [node.id, node]));
  const ready = nodes.filter((node) => indegree.get(node.id) === 0);
  const order: string[] = [];
  while (ready.length > 0) {
    ready.sort(byPosition);
    const node = ready.shift();
    if (!node) {
      break;
    }
    order.push(node.id);
    for (const edge of edges) {
      if (edge.source !== node.id) {
        continue;
      }
      const left = (indegree.get(edge.target) ?? 0) - 1;
      indegree.set(edge.target, left);
      const target = byId.get(edge.target);
      if (left === 0 && target) {
        ready.push(target);
      }
    }
  }

  const ordered = new Set(order);
  const rest = nodes.filter((node) => !ordered.has(node.id)).sort(byPosition);
  return [...order, ...rest.map((node) => node.id)];
};

export const scheduleRun = (
  nodes: WorkflowNodeData[],
  edges: WorkflowEdgeData[]
): WorkflowSchedule => {
  const order = getRunOrder(nodes, edges);
  const schedule: WorkflowSchedule = {
    edges: {},
    nodes: {},
    order,
    totalMs: 0,
  };

  const byId = new Map(nodes.map((node) => [node.id, node]));
  for (const id of order) {
    const node = byId.get(id);
    if (!node) {
      continue;
    }
    const incoming = edges
      .filter((edge) => edge.target === id)
      .map((edge) => schedule.edges[edge.id])
      .filter((run) => run !== undefined);
    const taken = incoming.filter((run) => run.taken);
    const settled = Math.max(0, ...incoming.map((run) => run.end));

    if (incoming.length > 0 && taken.length === 0) {
      schedule.nodes[id] = { end: settled, start: settled, status: "skipped" };
    } else {
      const start = Math.max(0, ...taken.map((run) => run.end));
      schedule.nodes[id] = {
        end: start + (node.durationMs ?? WORKFLOW_DEFAULT_DURATION_MS),
        start,
        status: node.fail ? "error" : "success",
      };
    }

    const run = schedule.nodes[id];
    for (const edge of edges.filter((item) => item.source === id)) {
      const port = edge.sourcePort ?? WORKFLOW_OUT_PORT.id;
      const isTaken =
        run.status === "success" &&
        (node.branch === undefined || node.branch === port);
      schedule.edges[edge.id] = {
        end: run.end + (isTaken ? WORKFLOW_EDGE_TRAVEL_MS : 0),
        start: run.end,
        taken: isTaken,
      };
    }
    schedule.totalMs = Math.max(schedule.totalMs, run.end);
  }

  return schedule;
};

export const getNodeState = (
  node: WorkflowNodeData | undefined,
  schedule: WorkflowSchedule,
  phase: WorkflowRunPhase,
  elapsedMs: number
): WorkflowNodeState => {
  const run = node ? schedule.nodes[node.id] : undefined;
  if (!(node && run) || phase === "idle") {
    return { elapsedMs: 0, status: "idle" };
  }
  if (elapsedMs < run.start) {
    return { elapsedMs: 0, status: "queued" };
  }
  if (run.status === "skipped") {
    return { elapsedMs: 0, status: "skipped" };
  }
  if (elapsedMs < run.end) {
    return {
      elapsedMs: elapsedMs - run.start,
      status: node.kind === "approval" ? "waiting" : "running",
    };
  }
  return { elapsedMs: run.end - run.start, status: run.status };
};

export const getEdgeState = (
  id: string,
  schedule: WorkflowSchedule,
  phase: WorkflowRunPhase,
  elapsedMs: number
): WorkflowEdgeStatus => {
  const run = schedule.edges[id];
  if (!run || phase === "idle" || elapsedMs < run.start) {
    return "idle";
  }
  if (!run.taken) {
    return "skipped";
  }
  return elapsedMs < run.end ? "active" : "done";
};

export const formatDuration = (ms: number) => {
  if (ms < 60_000) {
    return `${(ms / 1000).toFixed(1)}s`;
  }
  if (ms < 3_600_000) {
    const minutes = Math.floor(ms / 60_000);
    const seconds = Math.floor((ms % 60_000) / 1000);
    return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
  }
  const hours = Math.floor(ms / 3_600_000);
  const minutes = Math.floor((ms % 3_600_000) / 60_000);
  return `${hours}h ${String(minutes).padStart(2, "0")}m`;
};

export const formatRate = (rate: number) => `${Math.round(rate * 100)}%`;

export const formatClock = (ms: number) => {
  const minutes = Math.floor(ms / 60_000);
  const seconds = Math.floor((ms % 60_000) / 1000);
  const tenths = Math.floor((ms % 1000) / 100);
  return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${tenths}`;
};

const branchLabel = (node: WorkflowNodeData) =>
  getOutputPorts(node).find((port) => port.id === node.branch)?.label ??
  node.branch;

export const getRunLog = (
  nodes: WorkflowNodeData[],
  schedule: WorkflowSchedule,
  phase: WorkflowRunPhase,
  elapsedMs: number
): WorkflowLogEntry[] => {
  if (phase === "idle") {
    return [];
  }
  const entries: WorkflowLogEntry[] = [];
  const byId = new Map(nodes.map((node) => [node.id, node]));
  for (const id of schedule.order) {
    const node = byId.get(id);
    const run = schedule.nodes[id];
    if (!(node && run) || elapsedMs < run.start) {
      continue;
    }
    const base = { nodeId: id, title: node.title };
    if (run.status === "skipped") {
      entries.push({
        ...base,
        at: run.start,
        id: `${id}:skip`,
        level: "skip",
        message: "Skipped, no earlier step led here",
      });
      continue;
    }
    entries.push({
      ...base,
      at: run.start,
      id: `${id}:start`,
      level: "info",
      message: node.kind === "approval" ? "Waiting for approval" : "Started",
    });
    if (elapsedMs < run.end) {
      continue;
    }
    const took = run.end - run.start;
    let message = `Completed in ${formatDuration(took)}`;
    if (run.status === "error") {
      message = `Failed after ${formatDuration(took)}`;
    } else if (node.branch !== undefined) {
      message = `Took the "${branchLabel(node)}" branch`;
    }
    entries.push({
      ...base,
      at: run.end,
      id: `${id}:end`,
      level: run.status === "error" ? "error" : "success",
      message,
    });
  }
  return entries.sort((a, b) => a.at - b.at);
};

export const getEdgePath = (
  from: WorkflowPoint,
  to: WorkflowPoint,
  radius = WORKFLOW_EDGE_RADIUS
) => {
  const dx = to.x - from.x;
  const dy = to.y - from.y;
  const midX = from.x + dx / 2;
  const r = Math.min(radius, Math.abs(dy) / 2, Math.abs(dx) / 2);
  if (r < 0.5) {
    return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
  }
  const sx = Math.sign(dx);
  const sy = Math.sign(dy);
  const sweep = sx * sy > 0 ? 1 : 0;
  return [
    `M ${from.x} ${from.y}`,
    `L ${midX - sx * r} ${from.y}`,
    `A ${r} ${r} 0 0 ${sweep} ${midX} ${from.y + sy * r}`,
    `L ${midX} ${to.y - sy * r}`,
    `A ${r} ${r} 0 0 ${1 - sweep} ${midX + sx * r} ${to.y}`,
    `L ${to.x} ${to.y}`,
  ].join(" ");
};

export const getFlowEdgePath = (
  source: WorkflowAnchor,
  target: WorkflowAnchor,
  reverse = false
): [string, number, number] => {
  const [from, to] = reverse ? [target, source] : [source, target];
  if (target.x > source.x) {
    return [
      getEdgePath(from, to),
      (source.x + target.x) / 2,
      (source.y + target.y) / 2,
    ];
  }
  const [path, labelX, labelY] = getSmoothStepPath({
    ...WORKFLOW_EDGE_STEP_OPTIONS,
    sourcePosition: from.position,
    sourceX: from.x,
    sourceY: from.y,
    targetPosition: to.position,
    targetX: to.x,
    targetY: to.y,
  });
  return [path, labelX, labelY];
};

export const createNodeFromBlock = (
  block: WorkflowBlock,
  position: WorkflowPoint
): WorkflowNodeData => ({
  branch:
    block.outputs && block.outputs.length > 1 ? block.outputs[0].id : undefined,
  description: block.description,
  durationMs: block.durationMs,
  id: `${block.id}-${Date.now().toString(36)}`,
  inputs: block.inputs,
  kind: block.kind,
  logo: block.logo,
  model: block.model,
  outputs: block.outputs,
  position,
  title: block.title,
});
```

### `registry/notra/workflow/types/workflow.ts`

```ts
import type { Edge, Node, Position } from "@xyflow/react";
import type { ComponentProps, ComponentType, ReactNode, SVGProps } from "react";

export type WorkflowNodeKind =
  | "trigger"
  | "task"
  | "ai"
  | "integration"
  | "condition"
  | "switch"
  | "merge"
  | "loop"
  | "delay"
  | "approval"
  | "email"
  | "output";

export type WorkflowNodeStatus =
  | "idle"
  | "queued"
  | "running"
  | "waiting"
  | "success"
  | "error"
  | "skipped";

export type WorkflowEdgeStatus = "idle" | "active" | "done" | "skipped";

export type WorkflowRunPhase = "idle" | "running" | "done";

export type WorkflowLogoName =
  | "openai"
  | "anthropic"
  | "claude"
  | "gemini"
  | "perplexity"
  | "mistral"
  | "deepseek"
  | "grok"
  | "browserbase"
  | "context-dev"
  | "firecrawl"
  | "resend"
  | "slack"
  | "gmail"
  | "notion"
  | "google"
  | "google-sheets"
  | "webflow"
  | "wordpress"
  | "linear"
  | "github"
  | "ahrefs"
  | "posthog";

export type WorkflowLogLevel = "info" | "success" | "error" | "skip";

export type WorkflowIcon = ComponentType<SVGProps<SVGSVGElement>>;

export interface WorkflowPoint {
  x: number;
  y: number;
}

export interface WorkflowAnchor extends WorkflowPoint {
  position: Position;
}

export interface WorkflowPort {
  id: string;
  label?: string;
  hint?: string;
  rate?: number;
}

export interface WorkflowRule {
  subject: string;
  operator: string;
  value: string;
}

export interface WorkflowField {
  label: string;
  value: string;
}

export interface WorkflowNodeData {
  id: string;
  kind: WorkflowNodeKind;
  title: string;
  description?: string;
  position: WorkflowPoint;
  inputs?: WorkflowPort[];
  outputs?: WorkflowPort[];
  logo?: WorkflowLogoName;
  model?: string;
  durationMs?: number;
  branch?: string;
  rule?: WorkflowRule;
  fail?: boolean;
  fields?: WorkflowField[];
  output?: string;
}

export interface WorkflowEdgeData {
  id: string;
  source: string;
  sourcePort?: string;
  target: string;
  targetPort?: string;
  label?: string;
  animated?: boolean;
}

export interface WorkflowGraph {
  edges: WorkflowEdgeData[];
  nodes: WorkflowNodeData[];
}

export interface WorkflowTemplate extends WorkflowGraph {
  id: string;
  title: string;
  description: string;
  category: string;
}

export interface WorkflowKindConfig {
  icon: WorkflowIcon;
  label: string;
  tileClassName: string;
}

export interface WorkflowStatusConfig {
  className: string;
  label: string;
  ledClassName: string;
}

export interface WorkflowLogoConfig {
  label: string;
  logo: WorkflowIcon;
  tile?: boolean;
}

export interface WorkflowModel {
  id: string;
  label: string;
  logo: WorkflowLogoName;
  provider: string;
}

export interface WorkflowBlock {
  id: string;
  title: string;
  description: string;
  group: string;
  kind: WorkflowNodeKind;
  logo?: WorkflowLogoName;
  model?: string;
  inputs?: WorkflowPort[];
  outputs?: WorkflowPort[];
  durationMs?: number;
}

export interface WorkflowNodeRun {
  start: number;
  end: number;
  status: "success" | "error" | "skipped";
}

export interface WorkflowEdgeRun {
  start: number;
  end: number;
  taken: boolean;
}

export interface WorkflowSchedule {
  edges: Record<string, WorkflowEdgeRun>;
  nodes: Record<string, WorkflowNodeRun>;
  order: string[];
  totalMs: number;
}

export interface WorkflowNodeState {
  elapsedMs: number;
  status: WorkflowNodeStatus;
}

export interface WorkflowLogEntry {
  at: number;
  id: string;
  level: WorkflowLogLevel;
  message: string;
  nodeId: string;
  title: string;
}

export interface WorkflowRunSummary {
  id: string;
  durationMs: number;
  startedAt: string;
  status: "success" | "error" | "running";
  trigger: string;
  steps: number;
}

export interface WorkflowRun {
  edgeState: (id: string) => WorkflowEdgeStatus;
  elapsedMs: number;
  nodeState: (id: string) => WorkflowNodeState;
  phase: WorkflowRunPhase;
  play: () => void;
  schedule: WorkflowSchedule;
  stop: () => void;
  totalMs: number;
}

export type WorkflowLogoProps = Omit<SVGProps<SVGSVGElement>, "name"> & {
  name: WorkflowLogoName;
};

export interface WorkflowFlowNodeData extends Record<string, unknown> {
  node: WorkflowNodeData;
  state?: WorkflowNodeState;
}

export interface WorkflowFlowEdgeData extends Record<string, unknown> {
  animated?: boolean;
  reverse?: boolean;
  label?: string;
  status: WorkflowEdgeStatus;
}

export type WorkflowFlowNode = Node<WorkflowFlowNodeData, "workflow">;

export type WorkflowFlowEdge = Edge<WorkflowFlowEdgeData, "workflow">;

export type WorkflowCanvasBackgroundVariant =
  | "dots"
  | "lines"
  | "cross"
  | "grid"
  | "none";

export interface WorkflowCanvasBackgroundProps {
  gap?: number;
  variant?: WorkflowCanvasBackgroundVariant;
}

export type WorkflowNodeSizes = Record<
  string,
  { height: number; width: number }
>;

export interface WorkflowPortListProps {
  ports: WorkflowPort[];
  title: string;
}

export interface WorkflowControlsProps {
  toolbar?: ReactNode;
}

export interface WorkflowBannerConfig {
  className: string;
  label: string;
  message: string;
}

export interface WorkflowStatusGlyphProps {
  status: WorkflowNodeStatus;
}

export interface WorkflowTemplatePreviewProps {
  template: WorkflowTemplate;
}

export interface WorkflowCanvasProps {
  className?: string;
  focusId?: string;
  background?: WorkflowCanvasBackgroundVariant;
  gap?: number;
  nodes: WorkflowNodeData[];
  edges: WorkflowEdgeData[];
  nodeState?: (id: string) => WorkflowNodeState;
  edgeState?: (id: string) => WorkflowEdgeStatus;
  selectedId?: string;
  controls?: boolean;
  minimap?: boolean;
  toolbar?: ReactNode;
  minZoom?: number;
  maxZoom?: number;
  onSelectNode?: (id: string) => void;
  onNodeMove?: (id: string, position: WorkflowPoint) => void;
  onNodeRemove?: (id: string) => void;
  onConnect?: (edge: Omit<WorkflowEdgeData, "id">) => void;
  onDropBlock?: (blockId: string, position: WorkflowPoint) => void;
}

export type WorkflowNodeCardProps = ComponentProps<"div"> & {
  node: WorkflowNodeData;
  state?: WorkflowNodeState;
  selected?: boolean;
  connectable?: boolean;
  children?: ReactNode;
};

export interface WorkflowHandleProps {
  active?: boolean;
  className?: string;
  connectable?: boolean;
  portId: string;
  type: "source" | "target";
}

export type WorkflowEdgeProps = ComponentProps<"g"> & {
  path: string;
  reverse?: boolean;
  status?: WorkflowEdgeStatus;
  animated?: boolean;
};

export type WorkflowTimerProps = ComponentProps<"span"> & {
  ms: number;
  running?: boolean;
};

export type WorkflowStatusIndicatorProps = ComponentProps<"span"> & {
  status: WorkflowNodeStatus;
};

export type WorkflowStatusBadgeProps = ComponentProps<"span"> & {
  status: WorkflowNodeStatus;
};

export type WorkflowStatusPillProps = ComponentProps<"span"> & {
  status: WorkflowNodeStatus;
};

export type WorkflowDeterminismBadgeProps = ComponentProps<"span"> & {
  deterministic: boolean;
};

export type WorkflowRunButtonProps = Omit<
  ComponentProps<"button">,
  "children"
> & {
  phase: WorkflowRunPhase;
  progress?: number;
  onPlay?: () => void;
  onStop?: () => void;
};

export type WorkflowNodePaletteProps = ComponentProps<"div"> & {
  blocks: WorkflowBlock[];
  onAdd?: (block: WorkflowBlock) => void;
};

export interface WorkflowNodeBranchProps {
  connectable?: boolean;
  node: WorkflowNodeData;
  port: WorkflowPort;
  status: WorkflowNodeStatus;
}

export interface WorkflowNodeBannerProps {
  status: WorkflowNodeStatus;
}

export interface WorkflowNodeSummaryProps {
  children?: ReactNode;
  node: WorkflowNodeData;
}

export interface WorkflowNodeEngineProps {
  node: WorkflowNodeData;
}

export interface WorkflowNodeFooterProps {
  node: WorkflowNodeData;
  state?: WorkflowNodeState;
}

export interface WorkflowBranchSelectProps {
  node: WorkflowNodeData;
  outputs: WorkflowPort[];
  onBranchChange: (id: string, branch: string) => void;
}

export type WorkflowNodeSettingsProps = Pick<
  WorkflowNodeSheetProps,
  "onBranchChange" | "onModelChange"
> & {
  node: WorkflowNodeData;
};

export type WorkflowNodeSheetProps = WorkflowOverlayProps & {
  node?: WorkflowNodeData;
  state?: WorkflowNodeState;
  log?: WorkflowLogEntry[];
  onDelete?: (id: string) => void;
  onModelChange?: (id: string, model: string) => void;
  onBranchChange?: (id: string, branch: string) => void;
};

export type WorkflowRunLogProps = ComponentProps<"div"> & {
  entries: WorkflowLogEntry[];
  emptyLabel?: string;
};

export type WorkflowRunTimelineProps = ComponentProps<"div"> & {
  nodes: WorkflowNodeData[];
  schedule: WorkflowSchedule;
  elapsedMs: number;
  phase: WorkflowRunPhase;
};

export type WorkflowRunHistoryProps = Omit<
  ComponentProps<"div">,
  "onSelect"
> & {
  runs: WorkflowRunSummary[];
  selectedId?: string;
  onSelect?: (id: string) => void;
};

export interface WorkflowModelSelectProps {
  className?: string;
  defaultValue?: string;
  disabled?: boolean;
  id?: string;
  models?: WorkflowModel[];
  onValueChange?: (value: string) => void;
  value?: string;
}

export type WorkflowTemplateCardProps = ComponentProps<"div"> & {
  template: WorkflowTemplate;
  onUse?: (template: WorkflowTemplate) => void;
};

export type WorkflowTemplateDialogProps = WorkflowOverlayProps & {
  template?: WorkflowTemplate;
  onUse?: (template: WorkflowTemplate) => void;
};

export interface WorkflowBlockIconProps {
  block: Pick<WorkflowBlock, "kind" | "logo" | "model">;
  className?: string;
}

export interface WorkflowOverlayProps {
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  open?: boolean;
}

export interface WorkflowCanvasBackgroundOption {
  label: string;
  value: WorkflowCanvasBackgroundVariant;
}

export interface WorkflowNodeDemo {
  node: WorkflowNodeData;
  state: WorkflowNodeState;
}

export interface WorkflowEdgeDemo {
  animated?: boolean;
  label: string;
  status: WorkflowEdgeStatus;
}
```
