carolina

Draggable Widget Grid

Cards 2.4K

About

A draggable and rearrangeable widget grid for building customizable dashboards. Move widgets freely while the layout automatically adapts around them.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Draggable Widget Grid to your project and fits it to your theme.

Code

How Draggable Widget Grid is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

'use client'

import {
	createContext,
	Fragment,
	useContext,
	useEffect,
	useState,
	type ReactNode,
} from 'react'
import DraggableWidgetGrid, { type WidgetItem } from '@/components/ui/draggable-widget-grid'

/* ------------------------------------------------------------------ *
 * Demo: an AI agent observability dashboard with eight widgets.
 *
 * All data is simulated and updates every few seconds (paused when the user
 * prefers reduced motion). Updating values are not announced to screen
 * readers; every chart has a text alternative and every status has a label.
 * ------------------------------------------------------------------ */

type Kind =
	| 'runs'
	| 'health'
	| 'cost'
	| 'failures'
	| 'traces'
	| 'evals'
	| 'tools'
	| 'models'

interface Widget extends WidgetItem {
	kind: Kind
}

const WIDGETS: Widget[] = [
	{ id: 'runs', kind: 'runs', size: 'wide', label: 'Runs today' },
	{ id: 'health', kind: 'health', size: 'sm', label: 'System status' },
	{ id: 'cost', kind: 'cost', size: 'sm', label: 'Total cost' },
	{ id: 'failures', kind: 'failures', size: 'sm', label: 'Errors' },
	{ id: 'traces', kind: 'traces', size: 'wide', label: 'Recent traces' },
	{ id: 'evals', kind: 'evals', size: 'sm', label: 'Eval score' },
	{ id: 'tools', kind: 'tools', size: 'wide', label: 'Tool calls' },
	{ id: 'models', kind: 'models', size: 'wide', label: 'Token usage by model' },
]

/* ------------------------------------------------------------------ *
 * Live data
 * ------------------------------------------------------------------ */

const LiveContext = createContext(true)

/**
 * The demo's own palette, so the preview looks the same in any host theme.
 * The grid itself only uses theme tokens.
 */
const PALETTE = [
	'[--background:#ffffff] [--color-background:#ffffff] [--foreground:#09090b] [--color-foreground:#09090b] [--card:#ffffff] [--color-card:#ffffff] [--card-foreground:#09090b] [--color-card-foreground:#09090b] [--muted-foreground:#71717a] [--color-muted-foreground:#71717a] [--border:#e4e4e7] [--color-border:#e4e4e7] [--ring:#18181b] [--color-ring:#18181b]',
	'dark:[--background:#0a0a0b] dark:[--color-background:#0a0a0b] dark:[--foreground:#fafafa] dark:[--color-foreground:#fafafa] dark:[--card:#141417] dark:[--color-card:#141417] dark:[--card-foreground:#fafafa] dark:[--color-card-foreground:#fafafa] dark:[--muted-foreground:#a1a1aa] dark:[--color-muted-foreground:#a1a1aa] dark:[--border:#27272a] dark:[--color-border:#27272a] dark:[--ring:#d4d4d8] dark:[--color-ring:#d4d4d8]',
].join(' ')

const FONT_URL =
	'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap'
const FONT =
	"'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"

/** A counter that advances while live data is on and the page is visible. */
function useTick(ms = 2000) {
	const live = useContext(LiveContext)
	const [tick, setTick] = useState(0)
	useEffect(() => {
		if (!live) return
		const id = window.setInterval(() => {
			if (!document.hidden) setTick((t) => t + 1)
		}, ms)
		return () => window.clearInterval(id)
	}, [live, ms])
	return tick
}

/** Deterministic noise, so server and client agree on the first frame. */
function noise(seed: number) {
	const x = Math.sin(seed * 127.1 + 311.7) * 43758.5453
	return x - Math.floor(x)
}

const fmt = (v: number) => v.toLocaleString('en-US')

const median = (values: number[]) => {
	const sorted = [...values].sort((a, b) => a - b)
	const mid = Math.floor(sorted.length / 2)
	return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
}

const duration = (v: number) =>
	v >= 1000 ? `${(v / 1000).toFixed(2)}s` : `${Math.round(v)}ms`

/* ------------------------------------------------------------------ *
 * Palette: neutral theme tokens, blue for volume, status colours for status.
 * ------------------------------------------------------------------ */

type Tone = 'ok' | 'warn' | 'err' | 'idle'

const DOT: Record<Tone, string> = {
	ok: 'bg-emerald-500',
	warn: 'bg-amber-500',
	err: 'bg-rose-500',
	idle: 'bg-muted-foreground/60',
}

const TEXT: Record<Tone, string> = {
	ok: 'text-emerald-600 dark:text-emerald-400',
	warn: 'text-amber-600 dark:text-amber-300',
	err: 'text-rose-600 dark:text-rose-400',
	idle: 'text-muted-foreground',
}

const ACCENT = 'bg-blue-500 dark:bg-blue-400'

/** Heatmap levels, from empty to the busiest interval. */
const HEAT = [
	'bg-foreground/[0.06]',
	'bg-blue-500/20',
	'bg-blue-500/35',
	'bg-blue-500/55',
	'bg-blue-500/85 dark:bg-blue-400/85',
]

/* ------------------------------------------------------------------ *
 * Building blocks
 * ------------------------------------------------------------------ */

function Shell({
	title,
	meta,
	children,
}: {
	title: string
	meta?: ReactNode
	children: ReactNode
}) {
	return (
		<section className="@container flex h-full flex-col gap-4 p-4 sm:p-[22px]">
			<header className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2 text-[14px] leading-none">
				<h3 className="truncate text-[12px] tracking-[0.1em] text-muted-foreground uppercase">
					{title}
				</h3>
				{meta && <span className="shrink-0 text-muted-foreground">{meta}</span>}
			</header>
			<div className="flex min-h-0 flex-1 flex-col">{children}</div>
		</section>
	)
}

function Big({
	children,
	unit,
	unitWide = false,
}: {
	children: ReactNode
	unit?: string
	/** Only show the unit on tiles wide enough to keep it on one line. */
	unitWide?: boolean
}) {
	return (
		<p className="text-[28px] leading-none font-normal tracking-tight text-foreground tabular-nums @[240px]:text-[30px]">
			{children}
			{unit && (
				<span
					className={`text-[13px] tracking-normal text-muted-foreground ${
						unitWide ? 'sr-only @[200px]:not-sr-only' : ''
					}`}>
					{/* A real space: `not-sr-only` would wipe a margin. */}
					{'\u00a0'}
					{unit}
				</span>
			)}
		</p>
	)
}

/** A change against a previous period, said in words as well as an arrow. */
function Delta({
	value,
	against,
	suffix,
	good = 'up',
}: {
	value: number
	against: string
	/** Shown after the number when there is room, e.g. "MoM". */
	suffix?: string
	good?: 'up' | 'down'
}) {
	const up = value >= 0
	const tone: Tone = up === (good === 'up') ? 'ok' : 'err'
	return (
		<span className={`text-[14px] tabular-nums ${TEXT[tone]}`}>
			<span aria-hidden="true">{up ? '↑' : '↓'} </span>
			{Math.abs(value)}%
			{suffix && (
				<span aria-hidden="true" className="text-muted-foreground">
					{' '}
					{suffix}
				</span>
			)}
			<span className="sr-only"> {against}</span>
		</span>
	)
}

function Dot({ tone, pulse = false }: { tone: Tone; pulse?: boolean }) {
	return (
		<span aria-hidden="true" className="relative inline-flex size-2 shrink-0">
			{pulse && (
				<span
					className={`absolute inset-0 animate-ping rounded-full opacity-50 motion-reduce:hidden ${DOT[tone]}`}
				/>
			)}
			<span className={`relative size-2 rounded-full ${DOT[tone]}`} />
		</span>
	)
}

/** Label on the left, value on the right, one quiet line. */
function Row({
	children,
	value,
	className = '',
}: {
	children: ReactNode
	value: ReactNode
	className?: string
}) {
	return (
		<div className={`flex items-center gap-2 text-[13px] ${className}`}>
			<dt className="flex min-w-0 items-center gap-2 truncate text-foreground">
				{children}
			</dt>
			<dd className="ml-auto text-muted-foreground tabular-nums">{value}</dd>
		</div>
	)
}

/* ------------------------------------------------------------------ *
 * Runs: a minimap of the last five days, one cell per 45 minutes
 * ------------------------------------------------------------------ */

const DAYS = ['Sat', 'Sun', 'Mon', 'Tue', 'Today']
const SLOTS = 32
const SLOT_MINUTES = 45
/** 21:00 today, as a slot. Everything after it has not happened yet. */
const NOW = 28
const HOURS = ['12AM', '6AM', '12PM', '6PM']

function runsAt(day: number, slot: number) {
	const hour = (slot * SLOT_MINUTES) / 60
	const weekend = DAYS[day] === 'Sat' || DAYS[day] === 'Sun'
	// Never quite idle, a morning ramp, a long afternoon peak, quieter weekends.
	const shape =
		3.5 +
		Math.exp(-((hour - 15) ** 2) / 30) * 18 +
		Math.exp(-((hour - 10) ** 2) / 10) * 11
	return Math.max(
		0,
		Math.round(
			shape * 3.3 * (weekend ? 0.5 : 1) * (0.5 + noise(day * 97 + slot)),
		),
	)
}

const slotClock = (slot: number) => {
	const minutes = slot * SLOT_MINUTES
	return `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`
}

function Runs() {
	const t = useTick(2500)
	const today = DAYS.length - 1
	const grid = DAYS.map((_, d) =>
		Array.from({ length: SLOTS }, (_, s) =>
			d === today && s > NOW
				? null
				: runsAt(d, s) + (d === today && s === NOW ? t % 12 : 0),
		),
	)
	const peak = Math.max(...grid.flat().map((v) => v ?? 0))
	const total = grid[today].reduce<number>((a, v) => a + (v ?? 0), 0)

	return (
		<Shell
			title="Runs today"
			meta={
				<Delta
					value={12}
					against="compared with yesterday"
					suffix="vs yesterday"
				/>
			}>
			<Big>{fmt(total)}</Big>
			<div className="mt-auto">
				<div
					role="img"
					aria-label={`Runs per 45 minutes over the last 5 days. ${fmt(total)} runs so far today, busiest in the afternoon, quieter at the weekend.`}
					className="grid grid-cols-1 items-center gap-x-3 gap-y-[3px] @[480px]:grid-cols-[auto_minmax(0,1fr)]">
					{grid.map((row, d) => (
						<Fragment key={DAYS[d]}>
							<span
								aria-hidden="true"
								className={`hidden text-[12px] leading-none @[480px]:block ${
									d === today ? 'text-foreground' : 'text-muted-foreground'
								}`}>
								{DAYS[d]}
							</span>
							<span className="grid grid-cols-[repeat(32,minmax(0,1fr))] gap-[3px]">
								{row.map((v, s) => {
									const level =
										v === null || v === 0
											? 0
											: Math.max(1, Math.ceil((v / peak) * 4))
									return (
										<span
											key={s}
											title={
												v === null
													? undefined
													: `${DAYS[d]} ${slotClock(s)} · ${v} runs`
											}
											className={`aspect-square rounded-[2.5px] transition-colors duration-700 motion-reduce:transition-none ${
												d === today && s === NOW
													? 'bg-blue-600 dark:bg-blue-300'
													: HEAT[level]
											}`}
										/>
									)
								})}
							</span>
						</Fragment>
					))}
					{/* Day and hour labels only where the tile is tall enough. */}
					<span aria-hidden="true" className="hidden @[480px]:block" />
					<span
						aria-hidden="true"
						className="mt-2 hidden grid-cols-4 text-[12px] text-muted-foreground @[480px]:grid">
						{HOURS.map((h) => (
							<span key={h}>{h}</span>
						))}
					</span>
				</div>
			</div>
		</Shell>
	)
}

/* ------------------------------------------------------------------ *
 * System health: uptime as a status-page strip
 * ------------------------------------------------------------------ */

/** Days in the last 30 that had an incident, and how bad. */
const INCIDENTS: Record<number, Tone> = { 8: 'warn', 21: 'warn' }

function Health() {
	const t = useTick(5000)
	const degraded = t % 6 === 5
	return (
		<Shell title="System status" meta="30d">
			<Big unit="uptime" unitWide>
				99.98%
			</Big>
			<p
				className={`mt-3 flex items-center gap-2 text-[13px] ${
					degraded ? TEXT.warn : 'text-foreground'
				}`}>
				<Dot tone={degraded ? 'warn' : 'ok'} pulse />
				<span className="truncate">
					{degraded ? 'Degraded performance' : 'All systems operational'}
				</span>
			</p>
			<div className="mt-auto">
				<div
					role="img"
					aria-label="Uptime over the last 30 days: 28 days operational, 2 days with degraded performance."
					className="flex h-5 gap-[2px] @[240px]:h-6">
					{Array.from({ length: 30 }, (_, i) => (
						<span
							key={i}
							className={`flex-1 rounded-[1.5px] ${
								INCIDENTS[i] ? 'bg-amber-400/80' : 'bg-foreground/15'
							}`}
						/>
					))}
				</div>
			</div>
		</Shell>
	)
}

/* ------------------------------------------------------------------ *
 * Total cost
 * ------------------------------------------------------------------ */

function Cost() {
	const t = useTick(3000)
	const days = Array.from({ length: 14 }, (_, i) =>
		Math.round(9 + noise(i * 5) * 7 + i * 0.35),
	)
	// Today's bar and the month total drift, then wrap, so the scale holds.
	days[13] = Math.round(12 + (t % 30) * 0.2)
	const month = 184.2 + (t % 30) * 0.07
	const max = Math.max(...days)
	return (
		<Shell
			title="Total cost"
			meta={
				<Delta
					value={8}
					against="compared with last month"
					suffix="MoM"
					good="down"
				/>
			}>
			<Big unit="MTD">${month.toFixed(0)}</Big>
			<div
				role="img"
				aria-label={`Daily cost over the last 14 days, between $${Math.min(...days)} and $${max}. Today $${days[13]}.`}
				className="mt-auto flex h-10 items-end gap-[3px]">
				{days.map((d, i) => (
					<span
						key={i}
						className={`flex-1 rounded-full transition-[height] duration-700 motion-reduce:transition-none ${
							i === days.length - 1 ? ACCENT : 'bg-foreground/15'
						}`}
						style={{ height: `${(d / max) * 100}%` }}
					/>
				))}
			</div>
		</Shell>
	)
}

/* ------------------------------------------------------------------ *
 * Failures
 * ------------------------------------------------------------------ */

function Failures() {
	const t = useTick(5000)
	const causes = [
		{ name: 'timeout', count: 9 + (Math.floor(t / 3) % 5) },
		{ name: 'rate-limit', count: 7 },
		{ name: 'tool-error', count: 5 + (Math.floor(t / 5) % 4) },
	]
	const total = causes.reduce((a, c) => a + c.count, 0) + 2
	return (
		<Shell title="Errors" meta="24h">
			<Big unit={`${((total / 1262) * 100).toFixed(1)}% rate`}>{total}</Big>
			<dl className="mt-auto space-y-2">
				{causes.map((c, i) => (
					<Row key={c.name} value={c.count}>
						<Dot tone={i === 0 ? 'err' : 'idle'} />
						{c.name}
					</Row>
				))}
			</dl>
		</Shell>
	)
}

/* ------------------------------------------------------------------ *
 * Latest traces
 * ------------------------------------------------------------------ */

const AGENTS = [
	'research_agent',
	'support_agent',
	'code_agent',
	'planner',
	'reviewer',
]

function trace(n: number) {
	const r = noise(n * 3)
	const tone: Tone = r > 0.9 ? 'err' : r > 0.8 ? 'warn' : 'ok'
	return {
		n,
		id: `tr_${Math.floor(noise(n) * 0xffffff)
			.toString(16)
			.padStart(6, '0')}`,
		agent: AGENTS[Math.floor(noise(n * 7) * AGENTS.length)],
		ms: 400 + noise(n * 13) * 3200,
		tone,
	}
}

const TRACE_WORD: Record<Tone, string> = {
	ok: 'success',
	warn: 'slow',
	err: 'error',
	idle: 'pending',
}

function Traces() {
	const t = useTick(2200)
	const rows = Array.from({ length: 4 }, (_, i) => trace(t + 40 - i))
	const longest = 3600
	return (
		<Shell
			title="Recent traces"
			meta={
				<span className="flex items-center gap-1.5">
					<Dot tone="ok" pulse />
					live
				</span>
			}>
			<Big unit="p50">{duration(median(rows.map((r) => r.ms)))}</Big>
			<ol
				aria-label="Most recent agent runs"
				className="mt-auto space-y-2 text-[13px]">
				{rows.map((r, i) => (
					<li
						key={r.n}
						className={`grid-cols-[6px_minmax(0,1fr)_60px] items-center gap-3 @[440px]:grid-cols-[6px_84px_minmax(0,1fr)_26%_60px] ${
							i === 0 ? 'text-foreground' : 'text-muted-foreground'
						} ${
							// Short tiles keep the newest.
							i >= 3
								? 'hidden @[520px]:grid'
								: i === 2
									? 'hidden @[360px]:grid'
									: 'grid'
						}`}>
						<Dot tone={r.tone} />
						<span className="truncate">
							{r.id}
							<span className="sr-only">
								, {TRACE_WORD[r.tone]}, {r.agent},
							</span>
						</span>
						<span aria-hidden="true" className="hidden truncate @[440px]:block">
							{r.agent}
						</span>
						<span
							aria-hidden="true"
							className="hidden h-[3px] rounded-full bg-foreground/10 @[440px]:block">
							<span
								className={`block h-full rounded-full ${i === 0 ? ACCENT : 'bg-foreground/25'}`}
								style={{ width: `${(r.ms / longest) * 100}%` }}
							/>
						</span>
						<span className="text-right tabular-nums">{duration(r.ms)}</span>
					</li>
				))}
			</ol>
		</Shell>
	)
}

/* ------------------------------------------------------------------ *
 * Evals
 * ------------------------------------------------------------------ */

const EVALS = [
	{ name: 'Faithfulness', value: 0.94 },
	{ name: 'Relevancy', value: 0.89 },
	{ name: 'Correctness', value: 0.91 },
]

function Evals() {
	const score = EVALS.reduce((a, e) => a + e.value, 0) / EVALS.length
	return (
		<Shell title="Eval score">
			<div className="flex flex-wrap items-baseline gap-x-2.5 gap-y-1">
				<Big>{score.toFixed(2)}</Big>
				<span className={`text-[14px] tabular-nums ${TEXT.ok}`}>
					<span aria-hidden="true">↑ </span>0.03
					<span className="sr-only"> since the previous experiment</span>
				</span>
			</div>
			<dl className="mt-auto space-y-2">
				{EVALS.map((e) => (
					<Row key={e.name} value={e.value.toFixed(2)}>
						{e.name}
					</Row>
				))}
			</dl>
		</Shell>
	)
}

/* ------------------------------------------------------------------ *
 * Tool usage
 * ------------------------------------------------------------------ */

const TOOLS = [
	{ name: 'web_search', calls: 412 },
	{ name: 'code_interpreter', calls: 268 },
	{ name: 'sql_query', calls: 197 },
	{ name: 'retrieve_docs', calls: 143 },
]

function Tools() {
	const t = useTick(3000)
	const rows = TOOLS.map((tool, i) => ({
		...tool,
		calls: tool.calls + Math.floor((t % 50) * (4 - i) * 0.6),
	}))
	const max = Math.max(...rows.map((r) => r.calls))
	const total = rows.reduce((a, r) => a + r.calls, 0)
	return (
		<Shell title="Tool calls" meta="24h">
			<Big>{fmt(total)}</Big>
			<table className="mt-auto w-full table-fixed text-left text-[13px]">
				<caption className="sr-only">Tool calls in the last 24 hours</caption>
				<thead className="sr-only">
					<tr>
						<th scope="col">tool</th>
						<th scope="col">share</th>
						<th scope="col">calls</th>
					</tr>
				</thead>
				<tbody>
					{rows.map((r, i) => (
						<tr
							key={r.name}
							className={
								// Short tiles keep the busiest tools.
								i >= 3
									? 'hidden @[520px]:table-row'
									: i === 2
										? 'hidden @[360px]:table-row'
										: ''
							}>
							<th
								scope="row"
								className={`w-[140px] truncate py-[6px] pr-3 font-normal ${
									i === 0 ? 'text-foreground' : 'text-muted-foreground'
								}`}>
								{r.name}
							</th>
							<td className="py-[6px]">
								<span
									aria-hidden="true"
									className="block h-[3px] rounded-full bg-foreground/10">
									<span
										className={`block h-full rounded-full transition-[width] duration-700 motion-reduce:transition-none ${
											i === 0 ? ACCENT : 'bg-foreground/25'
										}`}
										style={{ width: `${(r.calls / max) * 100}%` }}
									/>
								</span>
							</td>
							<td className="w-[56px] py-[6px] text-right text-muted-foreground tabular-nums">
								{r.calls}
							</td>
						</tr>
					))}
				</tbody>
			</table>
		</Shell>
	)
}

/* ------------------------------------------------------------------ *
 * Model usage
 * ------------------------------------------------------------------ */

const MODELS = [
	{ name: 'claude-sonnet-5', share: 0.52, swatch: ACCENT },
	{
		name: 'claude-haiku-4.5',
		share: 0.27,
		swatch: 'bg-blue-500/55 dark:bg-blue-400/55',
	},
	{
		name: 'claude-opus-5',
		share: 0.13,
		swatch: 'bg-blue-500/30 dark:bg-blue-400/30',
	},
	{ name: 'embed-v3', share: 0.08, swatch: 'bg-foreground/20' },
]

function Models() {
	return (
		<Shell title="Token usage" meta="by model · 24h">
			<Big unit="tokens">12.3M</Big>
			<dl className="mt-auto grid grid-cols-2 gap-x-6 gap-y-2">
				{MODELS.map((m) => (
					<Row key={m.name} value={`${Math.round(m.share * 100)}%`}>
						<span
							aria-hidden="true"
							className={`size-1.5 shrink-0 rounded-full ${m.swatch}`}
						/>
						<span className="truncate">{m.name}</span>
					</Row>
				))}
			</dl>
			<div
				role="img"
				aria-label={`Share of tokens: ${MODELS.map((m) => `${m.name} ${Math.round(m.share * 100)}%`).join(', ')}.`}
				className="mt-4 flex h-[3px] gap-[3px]">
				{MODELS.map((m) => (
					<span
						key={m.name}
						className={`h-full rounded-full ${m.swatch}`}
						style={{ width: `${m.share * 100}%` }}
					/>
				))}
			</div>
		</Shell>
	)
}

/* ------------------------------------------------------------------ *
 * Board
 * ------------------------------------------------------------------ */

const VIEWS: Record<Kind, () => ReactNode> = {
	runs: Runs,
	health: Health,
	cost: Cost,
	failures: Failures,
	traces: Traces,
	evals: Evals,
	tools: Tools,
	models: Models,
}

const renderWidget = (item: Widget) => {
	const View = VIEWS[item.kind]
	return <View />
}

export default function Demo() {
	const [live, setLive] = useState(true)

	useEffect(() => {
		if (window.matchMedia('(prefers-reduced-motion: reduce)').matches)
			setLive(false)
	}, [])

	return (
		<main
			className={`flex min-h-screen w-full items-center justify-center bg-background px-4 py-12 text-foreground antialiased ${PALETTE}`}
			style={{ fontFamily: FONT }}>
			<link rel="stylesheet" href={FONT_URL} />
			<div className="w-full max-w-[1180px]">
				<p className="mb-4 text-[14px] text-muted-foreground">
					<span className="[@media(pointer:coarse)]:hidden">
						Drag and rearrange widgets to customize the layout.
					</span>
					<span className="hidden [@media(pointer:coarse)]:inline">
						Press and hold a widget, then drag to rearrange the layout.
					</span>
				</p>
				<section aria-labelledby="agent-observability-title">
					<h2 id="agent-observability-title" className="sr-only">
						Agent observability
					</h2>
					<LiveContext.Provider value={live}>
						<DraggableWidgetGrid
							items={WIDGETS}
							renderItem={(item) => renderWidget(item as Widget)}
						/>
					</LiveContext.Provider>
				</section>
			</div>
		</main>
	)
}

Dependencies

More by carolina

View all
  • Grid Pulse

    Grid Pulse

    343
  • Techstack

    Techstack

    174
  • Reactive Glow Grid

    Reactive Glow Grid

    38

More in Cards

View all
  • Stats cards with links

    Stats cards with links

    33
  • Course Design Cards

    Course Design Cards

    1.1K
  • Card

    Card

    70
  • Statistics Card 2

    Statistics Card 2

    264
  • Statistics Card 10

    Statistics Card 10

    263
  • Card

    Card

    17
  • Card Studio

    Card Studio

    9
  • The Item one

    The Item one

    3