Testimonial Canvas
refinery/testimonial-canvasFreeComponent
A infinite canvas testimonial component inspired by godly.
Live preview
live · rendered by the registry
Rendered by refinery on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://refinery.abhii.me/r/testimonial-canvas.jsonSource
1"use client";23import { motion, useMotionValue, useSpring } from "motion/react";4import { useRef, useState } from "react";56export interface Testimonial {7 id: string;8 quote: string;9 author: string;10 meta?: string;11 avatar?: string;12}1314export interface TestimonialCanvasProps {15 testimonials: Testimonial[];16 height?: string;17 initialOffset?: { x: number; y: number };18 hint?: string | false;19}2021const CARD_POSITIONS: [number, number][] = [22 [0, 0],23 [340, -60],24 [-340, 80],25 [160, 260],26 [-200, -260],27 [580, 260],28 [-540, -160],29 [200, -320],30 [-200, 360],31 [720, -100],32 [-600, 300],33 [500, 500],34 [-500, -460],35 [960, 360],36 [-800, 50],37];3839const CARD_MARGIN_X = 60;40const CARD_MARGIN_Y = 60;4142const vignette =43 "radial-gradient(ellipse 70% 70% at 50% 50%, transparent 40%, var(--vignette-color) 100%)";4445function clamp(value: number, min: number, max: number) {46 return Math.min(Math.max(value, min), max);47}4849function getPostion(index: number): [number, number] {50 if (index < CARD_POSITIONS.length) return CARD_POSITIONS[index];5152 const angle = index * 137.5 * (Math.PI / 180);53 const radius = 400 + index * 80;54 return [Math.cos(angle) * radius, Math.sin(angle) * radius];55}5657function getPanBounds(count: number) {58 const xs: number[] = [];59 const ys: number[] = [];6061 for (let i = 0; i < count; i++) {62 const [x, y] = getPostion(i);63 xs.push(x);64 ys.push(y);65 }6667 const maxPanX = (Math.max(...xs) - Math.min(...xs)) / 2 + CARD_MARGIN_X;68 const maxPanY = (Math.max(...ys) - Math.min(...ys)) / 2 + CARD_MARGIN_Y;69 return { maxPanX, maxPanY };70}7172function TestimonialCard({73 testimonial,74 position,75 index,76}: {77 testimonial: Testimonial;78 position: [number, number];79 index: number;80}) {81 return (82 <motion.div83 initial={{ opacity: 0, scale: 0.92 }}84 animate={{ opacity: 1, scale: 1 }}85 transition={{86 duration: 0.5,87 delay: index * 0.04,88 ease: [0.16, 1, 0.3, 1],89 }}90 className="absolute w-72 rounded-2xl border border-black/8 bg-black/1 p-5 backdrop-blur-sm will-change-transform dark:border-white/8 dark:bg-white/3"91 style={{92 left: position[0],93 top: position[1],94 transform: "translate(-50%, -50%)",95 }}96 >97 <p className="text-sm leading-relaxed text-black/70 dark:text-white/70">98 “{testimonial.quote}”99 </p>100 <div className="mt-4 flex items-center gap-3">101 {testimonial.avatar ? (102 <img103// … truncated
What it pulls in
npm packages
Files it writes
More from refinery
All 16 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Analog Clock Screensaveranalog-clock-screensaver | refinery | Components | Free | 1 dep | |
| Avatar Circleavatar-circle | refinery | Components | Free | 1 dep | |
| DVD Screensaverdvd-screensaver | refinery | Components | Free | 1 dep | |
| Floating Cardfloating-card | refinery | Components | Free | 1 dep | |
| Floating Navfloating-nav | refinery | Components | Free | 1 dep | |
| Glowy Buttonglowy-button | refinery | Components | Free | 1 dep | |
| Move To Topmove-to-top-button | refinery | Components | Free | 1 dep | |
| Notification Badgenotification-badge | refinery | Components | Free | 1 dep |
