\n {item.quote}\n
\n\n\n {item.author.name}\n
\n\n {item.author.role}\n
\n{favoritesLabel}
\n- \n {item.favorites!.map((fav, i) => (\n
- \n \n {fav.icon}\n \n {fav.label}\n \n ))}\n
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "featured-portrait-testimonial",
"type": "registry:ui",
"title": "Featured Portrait Testimonial",
"description": "An expanding-portrait testimonial carousel. Inactive cards show a grayscale portrait with a name strip; the active card morphs into a full quote card with author, dashed divider, and a row of favorite-feature tags. Width transitions and content crossfade are choreographed for clean, premium movement.",
"dependencies": [
"lucide-react"
],
"registryDependencies": [
"avatar"
],
"files": [
{
"path": "registry/ruixenui/featured-portrait-testimonial.tsx",
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { cn } from \"@/lib/utils\";\n\n/* ── types ───────────────────────────────────────────────────── */\n\nexport interface FeaturedPortraitFavorite {\n icon: React.ReactNode;\n label: React.ReactNode;\n}\n\nexport interface FeaturedPortraitAuthor {\n name: string;\n role: string;\n avatarUrl?: string;\n}\n\nexport interface FeaturedPortraitCompany {\n /** Subtitle rendered under the name in the inactive card footer. */\n name?: string;\n /** Tiny logo bubble drawn to the left of the avatar in the inactive footer. */\n logo?: React.ReactNode;\n}\n\nexport interface FeaturedPortraitItem {\n id: string;\n /** Pull-quote shown in the active (left-half) card. */\n quote: React.ReactNode;\n author: FeaturedPortraitAuthor;\n /** Portrait image used in the inactive card. */\n portraitUrl: string;\n /** Optional company subtitle + tiny logo bubble for the inactive footer. */\n company?: FeaturedPortraitCompany;\n /** Optional favorite-feature tags rendered under the dashed divider in the active card. */\n favorites?: FeaturedPortraitFavorite[];\n /** Tailwind classes overriding the active-card background. */\n accentClassName?: string;\n /** Tailwind classes overriding the inactive portrait container background tint. */\n portraitBgClassName?: string;\n}\n\nexport interface FeaturedPortraitTestimonialProps {\n items: FeaturedPortraitItem[];\n /** Small pill rendered above the heading. */\n eyebrow?: React.ReactNode;\n heading?: React.ReactNode;\n description?: React.ReactNode;\n /** Caption rendered above the favorites tag row in the active card. */\n favoritesLabel?: React.ReactNode;\n /** Initial active index. Default 0. */\n defaultIndex?: number;\n /** Width of each inactive portrait card in px. Default 240. */\n inactiveCardWidth?: number;\n className?: string;\n}\n\n/* ── motion constants ────────────────────────────────────────── */\n\nconst EASE = \"cubic-bezier(0.32, 0.72, 0, 1)\";\nconst SLIDE_MS = 650;\nconst FADE_MS = 450;\nconst GAP_PX = 16;\n\n/* ── component ───────────────────────────────────────────────── */\n\nexport function FeaturedPortraitTestimonial({\n items,\n eyebrow,\n heading,\n description,\n favoritesLabel = \"Favorites Feature\",\n defaultIndex = 0,\n inactiveCardWidth = 240,\n className,\n}: FeaturedPortraitTestimonialProps) {\n const total = items.length;\n const [activeIndex, setActiveIndex] = React.useState(() =>\n Math.min(Math.max(defaultIndex, 0), Math.max(0, total - 1)),\n );\n\n const activeItem = items[activeIndex];\n\n const goNext = React.useCallback(() => {\n if (total === 0) return;\n setActiveIndex((i) => (i + 1) % total);\n }, [total]);\n\n const goPrev = React.useCallback(() => {\n if (total === 0) return;\n setActiveIndex((i) => (i - 1 + total) % total);\n }, [total]);\n\n const onKey = React.useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowRight\") {\n e.preventDefault();\n goNext();\n } else if (e.key === \"ArrowLeft\") {\n e.preventDefault();\n goPrev();\n }\n },\n [goNext, goPrev],\n );\n\n if (total === 0 || !activeItem) return null;\n\n return (\n \n {description}\n \n {heading}\n
\n )}\n {description && (\n
\n {item.quote}\n
\n\n\n {item.author.name}\n
\n\n {item.author.role}\n
\n{favoritesLabel}
\n