# Link Preview Card

> URL preview card with a gradient icon, title, domain and description, plus a copy-link button showing Copied for 1.5s.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/link-preview-card
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/link-preview-card.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/link-preview-card-demo.tsx"
import { LinkPreviewCard } from "@/components/link-preview-card"

export default function LinkPreviewCardDemo() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <LinkPreviewCard
        url="https://www.example.com/blog/design-systems"
        title="Building a design system that scales"
        description="Lessons from growing a component library across twelve product teams."
      />
      <LinkPreviewCard
        url="https://docs.acme.dev/getting-started"
        title="Getting started with Acme"
        description="Install the CLI, create your first project and deploy in five minutes."
        iconClassName="from-sky-400 to-indigo-600"
      />
    </div>
  )
}
```

## Source

```tsx title="components/link-preview-card.tsx"
"use client"

import * as React from "react"
import { Check, Copy } from "lucide-react"

import { cn } from "@/lib/utils"

export interface LinkPreviewCardProps extends Omit<React.ComponentProps<"div">, "title"> {
  url: string
  title: string
  description?: string
  /** Tailwind gradient utilities for the favicon-like square */
  iconClassName?: string
}

function getDomain(url: string) {
  try {
    return new URL(url).hostname.replace(/^www\./, "")
  } catch {
    return url
  }
}

export function LinkPreviewCard({
  url,
  title,
  description,
  iconClassName = "from-orange-400 to-pink-600",
  className,
  ...props
}: LinkPreviewCardProps) {
  const [copied, setCopied] = React.useState(false)
  const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
  const domain = getDomain(url)

  React.useEffect(() => () => clearTimeout(timer.current), [])

  async function handleCopy() {
    try {
      await navigator.clipboard?.writeText(url)
    } catch {
      // Clipboard may be unavailable (insecure context or denied); still give feedback.
    }
    setCopied(true)
    clearTimeout(timer.current)
    timer.current = setTimeout(() => setCopied(false), 1500)
  }

  return (
    <div
      className={cn(
        "flex w-full max-w-sm items-start gap-4 rounded-2xl border bg-card p-4 text-card-foreground shadow-sm transition-shadow hover:shadow-md",
        className
      )}
      {...props}
    >
      <span
        className={cn(
          "flex size-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-lg font-bold uppercase text-white",
          iconClassName
        )}
        aria-hidden
      >
        {domain.charAt(0)}
      </span>
      <div className="min-w-0 flex-1">
        <a
          href={url}
          target="_blank"
          rel="noopener noreferrer"
          className="block truncate rounded-sm font-medium outline-none hover:underline focus-visible:ring-4 focus-visible:ring-ring/40"
        >
          {title}
        </a>
        <p className="truncate text-xs text-muted-foreground">{domain}</p>
        {description && <p className="mt-2 line-clamp-2 text-sm text-muted-foreground">{description}</p>}
      </div>
      <button
        type="button"
        onClick={handleCopy}
        aria-label={copied ? "Link copied" : "Copy link"}
        className={cn(
          "inline-flex h-8 shrink-0 items-center gap-1.5 rounded-lg border bg-background px-2.5 text-xs font-medium outline-none transition-colors hover:bg-accent focus-visible:ring-4 focus-visible:ring-ring/40",
          copied && "text-emerald-600 dark:text-emerald-400"
        )}
      >
        {copied ? <Check className="size-3.5" aria-hidden /> : <Copy className="size-3.5" aria-hidden />}
        <span aria-live="polite">{copied ? "Copied" : "Copy"}</span>
      </button>
    </div>
  )
}
```
