shadimage
000h-cojeev
uipreviewsource: ok · hit

Component preview

Pairs a live component example with a code view and a copy action.

npx shadcn add "https://000h.cojeev.com/r/preview.json"
free

Where it goes

testimonial

Visual style

terminal-mono

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light

Registry dependencies

https://cojeev.com/ui/r/cojeev.jsonhttps://cojeev.com/ui/r/code-block.jsonhttps://cojeev.com/ui/r/animated-icon.jsonhttps://cojeev.com/ui/r/tabs.jsonhttps://cojeev.com/ui/r/typography.jsonhttps://cojeev.com/ui/r/pattern-background.jsonhttps://cojeev.com/ui/r/pigment-field.jsonhttps://cojeev.com/ui/r/depth-background.jsonhttps://cojeev.com/ui/r/ambient-background.jsonhttps://cojeev.com/ui/r/button.jsonhttps://cojeev.com/ui/r/icon.jsonhttps://cojeev.com/ui/r/popover.json

Files4 · source cached 10/14/2026, 1:32:19 PM

registry/cojeev/ui/preview.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { CodeBlock, CopyButton } from "@/components/ui/code-block";
import { AnimatedIcon } from "@/components/ui/animated-icon";
import { PreviewBackgroundPaint, type PreviewBackgroundVariant } from "@/lib/cojeev/preview-backgrounds";
export type { PreviewBackgroundVariant } from "@/lib/cojeev/preview-backgrounds";
import { PreviewBackgroundPicker } from "@/lib/cojeev/preview-background-picker";
import {
  Tabs,
  TabsList,
  TabsTrigger,
  TabsContent,
} from "@/components/ui/tabs";
import { Title, BodySecondary } from "@/components/ui/typography";
export type PreviewProps = React.ComponentProps<"section"> & {
  code: string;
  title?: string;
  description?: string;
  /** Optional controls shown with the live specimen. */
  controls?: React.ReactNode;
  /** Optional actions beside the view switcher. */
  actions?: React.ReactNode;
  /** Controlled background. Without onPatternChange the background is fixed. */
  pattern?: PreviewBackgroundVariant;
  /** Initial background for an uncontrolled preview. */
  defaultPattern?: PreviewBackgroundVariant;
  onPatternChange?: (pattern: PreviewBackgroundVariant) => void;
  showBackgroundPicker?: boolean;
  /** Reports the source copy outcome without coupling the registry to analytics. */
  onCopyResult?: (result: "success" | "failure") => void;
};
export function Preview({
  children,
  code,
  title,
  description,
  controls,
  actions,
  pattern,
  defaultPattern = "dots",
  onPatternChange,
  showBackgroundPicker = true,
  onCopyResult,
  className,
  ...props
}: PreviewProps) {
  const [localPattern, setLocalPattern] = React.useState(defaultPattern);
  const [view, setView] = React.useState("preview");
  const selectedPattern = pattern ?? localPattern;
  const changePattern = (next: PreviewBackgroundVariant) => {
    if (pattern === undefined) setLocalPattern(next);
    onPatternChange?.(next);
  };
  return (
    <section
      data-slot="preview"
      className={cn("v-preview", className)}
      {...props}
    >
      {title && <Title as="h3">{title}</Title>}
      {description && <BodySecondary>{description}</BodySecondary>}
      <Tabs value={view} onValueChange={setView} variant="pills" className="v-preview__frame">
        <div className="v-preview__toolbar">
          <TabsList aria-label={title ? `${title} view` : "Example view"}>
            <TabsTrigger value="preview"><AnimatedIcon name="eye" size="sm" aria-hidden="true" />Preview</TabsTrigger>
            <TabsTrigger value="code"><AnimatedIcon name="code" size="sm" aria-hidden="true" />Code</TabsTrigger>
          </TabsList>
          <div className="v-preview__actions" role="group" aria-label="Example tools">
            {showBackgroundPicker && <PreviewBackgroundPicker value={selectedPattern} onChange={changePattern} disabled={view !== "preview" || (pattern !== undefined && !onPatternChange)} />}
            {actions}
          </div>
          <CopyButton code={code} onCopyResult={onCopyResult}>Copy code</CopyButton>
        </div>
        <TabsContent value="preview" forceMount hidden={view !== "preview"} className="v-preview__panel">
          {controls && <div className="v-preview__controls">{controls}</div>}
          <div
            data-slot="preview-canvas"
            className="v-preview__canvas"
            style={
              { "--muted-foreground": "var(--v-text-2)" } as React.CSSProperties
            }
          >
            <PreviewBackgroundPaint value={selectedPattern} />
            {children}
          </div>
        </TabsContent>
        <TabsContent value="code" forceMount hidden={view !== "code"} className="v-preview__panel">
          <CodeBlock embedded code={code} language="tsx" title="Example" />
        </TabsContent>
      </Tabs>
    </section>
  );
}
registry/cojeev/lib/preview-backgrounds.tsx
"use client";

import * as React from "react";
import { PatternBackground } from "@/components/ui/pattern-background";
import { useChoreography } from "@/lib/cojeev-motion/choreography";
import { subtleBackgrounds, type PatternBackgroundVariant } from "@/lib/cojeev/subtle-backgrounds";

const PigmentField = React.lazy(() => import("@/components/ui/pigment-field").then(module => ({ default: module.PigmentField })));
const DepthBackground = React.lazy(() => import("@/components/ui/depth-background").then(module => ({ default: module.DepthBackground })));
const AmbientBackground = React.lazy(() => import("@/components/ui/ambient-background").then(module => ({ default: module.AmbientBackground })));

const livingBackgrounds = [
  { value: "pigment", label: "Pigment wash", description: "Slowly blending color with a fine pigment texture." },
  { value: "pollen", label: "Pollen", description: "Soft pieces floating at different depths." },
  { value: "depth-contour", label: "Depth contours", description: "Layered organic contours around the edges." },
  { value: "orbital", label: "Orbital", description: "Small orbiting shapes with perspective." },
  { value: "ambient-drift", label: "Ambient drift", description: "Sparse shapes drifting past the canvas edges." },
  { value: "ambient-orbit", label: "Ambient orbit", description: "A slow orbit of soft, colored shapes." },
  { value: "ambient-contour", label: "Ambient contours", description: "Broad curved bands framing the content." },
] as const;

export type PreviewBackgroundVariant = PatternBackgroundVariant | typeof livingBackgrounds[number]["value"];
export const previewBackgroundGroups = [
  { label: "Atmosphere", choices: livingBackgrounds },
  { label: "Patterns", choices: subtleBackgrounds },
] as const;

/** Swatches are still CSS/SVG paint, never seven additional animation engines. */
export function PreviewBackgroundThumbnail({ value }: { value: PreviewBackgroundVariant }) {
  if (subtleBackgrounds.some(choice => choice.value === value)) {
    return <PatternBackground variant={value as PatternBackgroundVariant} spacing={12} opacity={.25} />;
  }
  return <span className="v-preview-background-thumbnail" data-preview-thumbnail={value} aria-hidden="true" />;
}

function LivingPreviewBackground({ value }: { value: Exclude<PreviewBackgroundVariant, PatternBackgroundVariant> }) {
  const { quiet } = useChoreography();
  let content: React.ReactNode;
  if (value === "pigment") content = <PigmentField speed={.45} intensity={.8} paused={quiet} />;
  else if (value === "pollen" || value === "depth-contour" || value === "orbital") {
    content = <DepthBackground variant={value === "depth-contour" ? "contour" : value} density={.6} intensity={quiet ? 0 : .45} seed="preview" />;
  } else {
    content = <AmbientBackground variant={value === "ambient-orbit" ? "orbit" : value === "ambient-contour" ? "contour" : "drift"} paused={quiet} />;
  }
  return <div data-preview-background={value} className="v-preview-background" aria-hidden="true" inert>
    <React.Suspense fallback={<PreviewBackgroundThumbnail value={value} />}>{content}</React.Suspense>
  </div>;
}

/** Only the chosen field mounts. The specimen stays a separate, stable sibling. */
export function PreviewBackgroundPaint({ value }: { value: PreviewBackgroundVariant }) {
  if (value === "none") return null;
  if (livingBackgrounds.some(choice => choice.value === value)) {
    return <LivingPreviewBackground value={value as Exclude<PreviewBackgroundVariant, PatternBackgroundVariant>} />;
  }
  return <PatternBackground variant={value as PatternBackgroundVariant} opacity={.12} />;
}
registry/cojeev/lib/preview-background-picker.tsx
"use client";

import * as React from "react";
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { PreviewBackgroundThumbnail, previewBackgroundGroups, type PreviewBackgroundVariant } from "@/lib/cojeev/preview-backgrounds";

/** One visual choice, outside document flow; selecting paint never remounts content. */
export function PreviewBackgroundPicker({ value, onChange, disabled = false }: {
  value: PreviewBackgroundVariant;
  onChange: (value: PreviewBackgroundVariant) => void;
  disabled?: boolean;
}) {
  const [open, setOpen] = React.useState(false);
  const choiceButton = (choice: { value: PreviewBackgroundVariant; label: string; description: string }) => <Button key={choice.value} shape="card" variant="ghost" className="v-preview-background-picker__choice" aria-label={choice.label} aria-pressed={value === choice.value} title={choice.description}
    onClick={() => { onChange(choice.value); setOpen(false); }}>
    <span className="v-preview-background-picker__swatch" aria-hidden="true">
      {choice.value === "none" ? <Icon name="ban" size="sm" /> : <PreviewBackgroundThumbnail value={choice.value} />}
    </span>
    <span className="v-preview-background-picker__label">{choice.label}<span className="v-preview-background-picker__check" aria-hidden="true">{value === choice.value && <Icon name="check" size="sm" />}</span></span>
  </Button>;
  return <Popover open={open} onOpenChange={setOpen}>
    <PopoverTrigger asChild>
      <Button size="sm" variant="ghost" disabled={disabled} title={disabled ? "Background is fixed here, or switch to Preview to change it." : "Choose a preview background"}>
        <Icon name="paintbrush" size="sm" aria-hidden="true" />Background
      </Button>
    </PopoverTrigger>
    <PopoverContent align="end" collisionPadding={12} className="v-preview-background-picker" aria-label="Choose preview background">
      <div className="v-preview-background-picker__heading">
        <strong>Preview backgrounds</strong>
        <span>Change the canvas. Keep your example.</span>
      </div>
      <div className="v-preview-background-picker__plain">{choiceButton({ value: "none", label: "Plain", description: "No decorative background." })}</div>
      {previewBackgroundGroups.map(group => <div key={group.label} className="v-preview-background-picker__group" role="group" aria-label={group.label}>
        <strong className="v-preview-background-picker__group-label">{group.label}</strong>
        <div className="v-preview-background-picker__choices">{group.choices.map(choiceButton)}</div>
      </div>)}
    </PopoverContent>
  </Popover>;
}
registry/cojeev/styles/preview.css
@layer cojeev-states {
@layer cojeev-states {
  .v-preview {
    display: grid;
    gap: var(--s-4);
    min-width: 0;
    container: preview / inline-size;
  }
  .v-preview__frame {
    min-width: 0;
    border: 1px solid color-mix(in srgb, var(--v-border) 72%, transparent);
    border-radius: var(--r-card-sm);
    background: var(--v-paper);
  }
  .v-preview__toolbar {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    border-radius: calc(var(--r-card-sm) - 1px) calc(var(--r-card-sm) - 1px) 0 0;
    background: var(--v-paper);
    border-block-end: 1px solid
      color-mix(in srgb, var(--v-border) 60%, transparent);
  }
  .v-preview__toolbar > [data-slot="tabs-list"] {
    align-self: stretch;
    align-items: center;
    gap: var(--s-1);
    padding: var(--s-1);
  }
  .v-preview__toolbar > [data-slot="tabs-list"] > [data-slot="tabs-trigger"] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    min-height: 40px;
    padding-block: var(--s-2);
    font-size: var(--fs-small);
  }
  .v-preview__panel {
    min-width: 0;
  }
  .v-preview__actions {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--s-2);
    min-width: 0;
    justify-content: end;
  }
  .v-preview__controls {
    padding: var(--s-4) var(--s-5);
    border-block-end: 1px solid var(--v-border);
  }
  .v-preview .v-preview__canvas {
    position: relative;
    isolation: isolate;
    display: grid;
    align-content: center;
    min-width: 0;
    min-height: 280px;
    margin: 0;
    overflow: visible;
    padding: clamp(20px, 4vw, 48px);
    border: 0;
    border-radius: 0 0 var(--r-card-sm) var(--r-card-sm);
    background: color-mix(in srgb, var(--v-paper) 96%, var(--v-beige));
    color: var(--v-text);
  }
  .v-preview__canvas > .v-btn {
    justify-self: start;
    max-width: 100%;
  }
  @media (max-width: 600px) {
    .v-preview__toolbar,
    .v-preview__controls {
      padding-inline: var(--s-4);
    }
    .v-preview .v-preview__canvas {
      min-height: 240px;
      padding: var(--s-5) var(--s-4);
    }
  }
}

.v-preview__panel[hidden] {
  display: none;
}
.v-preview__toolbar > [data-slot="copy-control"] {
  justify-self: end;
}
.v-preview__toolbar .docs-playground-actions {
  display: contents;
}
.v-preview__toolbar .v-btn {
  font-size: var(--fs-meta);
  min-height: 40px;
  padding-inline: var(--s-3);
  gap: var(--s-2);
}
.v-preview__toolbar [data-slot="icon"] {
  width: 17px;
  height: 17px;
}
@container preview (max-width: 850px) {
  .v-preview__toolbar {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--s-3);
  }
  .v-preview__toolbar > [data-slot="tabs-list"] {
    justify-self: start;
  }
  .v-preview__toolbar > [data-slot="copy-control"] {
    grid-column: 2;
    grid-row: 1;
  }
  .v-preview__actions {
    grid-column: 1 / -1;
    grid-row: 2;
    justify-content: start;
    padding-top: var(--s-3);
    border-top: 1px solid color-mix(in srgb, var(--v-border) 50%, transparent);
  }
}
@container preview (max-width: 460px) {
  .v-preview__toolbar {
    padding: var(--s-3);
  }
  .v-preview__toolbar > [data-slot="tabs-list"] {
    gap: 2px;
    padding: 0;
  }
  .v-preview__toolbar > [data-slot="tabs-list"] > [data-slot="tabs-trigger"] {
    gap: var(--s-1);
    padding-inline: var(--s-2);
    font-size: var(--fs-meta);
  }
  .v-preview__toolbar > [data-slot="copy-control"] .v-btn {
    padding-inline: var(--s-2);
  }
  .v-preview__actions {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--s-2);
  }
  .v-preview__actions .v-btn {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    min-width: 0;
    width: 100%;
    min-height: 60px;
    padding: var(--s-2) 3px;
    white-space: normal;
    text-align: center;
    line-height: 1.25;
    border-radius: var(--r-md);
  }
}

/* Preview joins its tab bar and canvas into one frame. */
@container preview (max-width: 350px) {
  .v-preview__toolbar {
    grid-template-columns: minmax(0, 1fr);
  }
  .v-preview__toolbar > [data-slot="tabs-list"] {
    width: 100%;
    justify-content: center;
    overflow: visible;
  }
  .v-preview__toolbar > [data-slot="copy-control"] {
    grid-column: 1;
    grid-row: 2;
  }
  .v-preview__actions {
    grid-row: 3;
  }
}
[data-slot="tabs"].v-preview__frame {
  gap: 0;
}

/* The host owns embedded code corners; keep this alongside CodeBlock's base layer. */
.v-preview__panel > .v-code-block[data-slot="code-block"][data-embedded] {
  border-radius: 0 0 calc(var(--r-card-sm) - 1px) calc(var(--r-card-sm) - 1px);
}

.v-preview-background-picker[data-slot="popover-content"] {
  width: min(392px, calc(100vw - 24px));
  max-height: min(620px, var(--radix-popover-content-available-height));
  overflow: auto;
  padding: var(--s-4);
  border-radius: var(--r-card-sm);
  background: var(--v-paper);
  color: var(--v-text);
}
.v-preview-background-picker__heading {
  display: grid;
  gap: var(--s-1);
  margin-bottom: var(--s-4);
}
.v-preview-background-picker__heading strong {
  font: var(--fw-semibold) var(--fs-body)/1.4 var(--font-text);
}
.v-preview-background-picker__heading > span {
  font: var(--fs-meta)/1.5 var(--font-text);
  color: var(--v-text-2);
}
.v-preview-background-picker__group {
  margin-top: var(--s-4);
}
.v-preview-background-picker__group-label {
  display: block;
  margin-bottom: var(--s-2);
  font: var(--fw-medium) var(--fs-meta)/1.4 var(--font-text);
  color: var(--v-text-2);
}
.v-preview-background-picker__choices {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-2);
}
.v-preview-background-picker__choice.v-btn {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: start;
  gap: var(--s-2);
  height: auto;
  min-width: 0;
  min-height: 86px;
  padding: var(--s-1);
  border-radius: var(--r-md);
  text-align: start;
  white-space: normal;
}
.v-preview-background-picker__choice[aria-pressed="true"] {
  background: var(--v-beige);
  box-shadow: inset 0 0 0 1px var(--v-text-3);
}
.v-preview-background-picker__swatch {
  position: relative;
  isolation: isolate;
  display: grid;
  place-items: center;
  overflow: hidden;
  width: 100%;
  height: 52px;
  border-radius: var(--r-sm);
  background: var(--v-canvas);
  color: var(--v-text-3);
}
.v-preview-background-picker__label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2px;
  min-width: 0;
  color: var(--v-text);
  font: var(--fs-meta)/1.4 var(--font-text);
}
.v-preview-background-picker__check {
  display: grid;
  place-items: center;
  width: 14px;
  height: 16px;
  flex: none;
}
.v-preview-background-picker__check [data-slot="icon"] {
  width: 13px;
  height: 13px;
}
.v-preview-background-picker__plain .v-preview-background-picker__choice {
  width: 100%;
  min-height: 40px;
  flex-direction: row;
  align-items: center;
  padding: var(--s-2) var(--s-3);
}
.v-preview-background-picker__plain .v-preview-background-picker__swatch {
  width: 24px;
  height: 24px;
  background: none;
  flex: none;
}
.v-preview-background-picker__plain .v-preview-background-picker__label {
  flex: 1;
}

/* A single selected library field, behind stable foreground content. */
.v-preview-background {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
  user-select: none;
  opacity: 0.46;
}
.v-preview-background > [data-slot="ambient-background"] {
  position: absolute;
  inset: 0;
  min-block-size: 0;
  padding: 0;
  border-radius: inherit;
  background: transparent;
}
.v-preview-background
  > [data-slot="ambient-background"]
  [data-slot="ambient-background-decoration"] {
  opacity: 0.65;
}
.v-preview-background-thumbnail {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  overflow: hidden;
}
.v-preview-background-thumbnail[data-preview-thumbnail="pigment"] {
  background:
    radial-gradient(
      ellipse at 12% 30%,
      color-mix(in srgb, var(--v-pink) 65%, transparent),
      transparent 65%
    ),
    radial-gradient(
      ellipse at 80% 90%,
      color-mix(in srgb, var(--v-blue) 65%, transparent),
      transparent 65%
    ),
    radial-gradient(
      ellipse at 75% 0%,
      color-mix(in srgb, var(--v-yellow) 45%, transparent),
      transparent 60%
    );
}
.v-preview-background-thumbnail[data-preview-thumbnail="pollen"] {
  background:
    radial-gradient(circle at 20% 30%, var(--v-pink) 2px, transparent 6px),
    radial-gradient(circle at 80% 64%, var(--v-blue) 3px, transparent 7px),
    radial-gradient(circle at 16% 88%, var(--v-yellow) 1px, transparent 4px),
    radial-gradient(circle at 78% 10%, var(--v-olive) 2px, transparent 3px);
}
.v-preview-background-thumbnail[data-preview-thumbnail="depth-contour"] {
  background: repeating-radial-gradient(
    ellipse at -15% 40%,
    transparent 0 9px,
    color-mix(in srgb, var(--v-blue) 50%, transparent) 10px 11px,
    transparent 12px 18px
  );
}
.v-preview-background-thumbnail[data-preview-thumbnail="orbital"] {
  background:
    radial-gradient(
      ellipse 16px 8px at 22% 40%,
      transparent 80%,
      var(--v-blue) 85% 92%,
      transparent 98%
    ),
    radial-gradient(
      ellipse 8px 16px at 22% 40%,
      transparent 80%,
      var(--v-pink) 85% 92%,
      transparent 98%
    ),
    radial-gradient(
      ellipse 14px 6px at 80% 70%,
      transparent 80%,
      var(--v-yellow) 85% 92%,
      transparent 98%
    );
}
.v-preview-background-thumbnail[data-preview-thumbnail^="ambient-"]::before,
.v-preview-background-thumbnail[data-preview-thumbnail^="ambient-"]::after {
  content: "";
  position: absolute;
  width: 60%;
  height: 95%;
  border-radius: 42% 58% 64% 36%;
  background: var(--v-pink);
  opacity: 0.4;
  right: -12%;
  top: -30%;
  transform: rotate(25deg);
}
.v-preview-background-thumbnail[data-preview-thumbnail^="ambient-"]::after {
  left: -15%;
  top: 60%;
  background: var(--v-olive);
}
.v-preview-background-thumbnail[data-preview-thumbnail="ambient-orbit"]::before {
  right: 25%;
  top: -65%;
  background: var(--v-yellow);
  border-radius: 50%;
}
.v-preview-background-thumbnail[data-preview-thumbnail="ambient-orbit"]::after {
  left: 55%;
  top: 45%;
  background: var(--v-blue);
  border-radius: 50%;
}
.v-preview-background-thumbnail[data-preview-thumbnail="ambient-contour"] {
  background: repeating-radial-gradient(
    ellipse at 110% -40%,
    transparent 0 10px,
    color-mix(in srgb, var(--v-olive) 35%, transparent) 11px 15px,
    transparent 16px 24px
  );
}
.v-preview-background-thumbnail[data-preview-thumbnail="ambient-contour"]::before,
.v-preview-background-thumbnail[data-preview-thumbnail="ambient-contour"]::after {
  display: none;
}
@media (forced-colors: active) {
  .v-preview-background,
  .v-preview-background-thumbnail {
    display: none;
  }
}

}

More from 000h-cojeev

Browse all components in this library →