Skip to content

feat(registry): add hero-radial-burst block with time-of-day themes#76

Merged
pras75299 merged 3 commits into
mainfrom
feat/hero-radial-burst
Jun 5, 2026
Merged

feat(registry): add hero-radial-burst block with time-of-day themes#76
pras75299 merged 3 commits into
mainfrom
feat/hero-radial-burst

Conversation

@pras75299

@pras75299 pras75299 commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Type

  • New component / block (registry)

Summary

A Stripe-style hero block built from the provided reference designs: a canvas radial burst of fine rays rising from a bright bottom-center core, beneath a headline, with six time-of-day themes (Pre-dawn, Sunrise, Daytime, Dusk, Sunset, Night) selectable from an in-block switcher.

Screenshots / video (UI changes only)

Canvas/animated — not a single static frame. Verified by rendering the live block and screenshotting Night, Sunrise, Daytime, and Sunset (all match the reference palettes). Switching themes crossfades the background gradient and eases the burst colors; fibers continuously grow, fade, and respawn; reduced-motion renders a calm static frame.

Test plan

  • pnpm test (66 repo + 92 www tests)
  • pnpm build:registry — diff limited to hero-radial-burst + aggregates (cross-link baseline test still passes)
  • pnpm registry:validate (65 entries) · pnpm check:reduced-motion (covered)
  • pnpm --dir apps/www build (202/202 static pages) · tsc --noEmit · eslint
  • Rendered live in 4 themes via Playwright and compared against the reference images

New component checklist

Registry sources

  • registry/blocks/hero/radial-burst/component.tsx created
  • registry/components/hero-radial-burst.json created — registry block (deps incl. lucide-react) + docs (name, description, category, props)
  • Slug added to order/docsOrder via pnpm new:component
  • registry/blocks/hero/radial-burst/demo.tsx added + key appended to registry/demos/demo-key-order.json; RadialBurstHero import added to registry/demos/shared.tsx
  • pnpm build:registry run — no unexpected generated diff

Component quality gate

  • "use client" (hooks + canvas)
  • Accepts className and merges via cn
  • Props extend the correct DOM type (Omit<ComponentProps<"section">, …>)
  • Uses motion from motion/reactuseMotionValue/animate drive the burst intro fade and theme color blend; motion variants for chrome entrance; AnimatePresence for the dropdown
  • Honors useReducedMotion — static full burst, final stat values, no rAF
  • No layout shift on mount — absolutely-positioned canvas + background
  • rAF and ResizeObserver cancelled/disconnected on unmount; dropdown listeners removed on close
  • SSR-safe — no window/document at module scope; getContext null-guarded
  • Switcher is keyboard-accessible (button + role=listbox/option, Escape + outside-click close); decorative canvas is aria-hidden
  • No cross-imports; only declared dependencies (motion, lucide-react, clsx, tailwind-merge)
  • Render coverage via apps/www/tests/blocks.test.tsx (auto-includes registered demos)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Radial Burst Hero: canvas-powered animated background with six time-of-day themes, pointer-driven interaction, in-hero theme switcher, and reduced-motion support.
  • Changes
    • Theme crossfades and animated headline; hero exposes burstProps (density, interactive) and defaults to "night"; stats/count-up UI removed.
  • Documentation
    • Component demos, registry entries, docs scenarios and changelogs added/updated (includes theme switcher accessibility update to a disclosure-style menu).

@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
uniqueui-platform Ready Ready Preview, Comment Jun 5, 2026 3:01pm

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b9366fca-b1c0-4ff6-8742-624fa182787d

📥 Commits

Reviewing files that changed from the base of the PR and between 48781de and c5230ed.

📒 Files selected for processing (8)
  • apps/www/components/ui/hero-radial-burst.tsx
  • apps/www/public/r/hero-radial-burst.json
  • apps/www/public/registry.json
  • apps/www/public/registry/changelogs.json
  • apps/www/public/registry/hero-radial-burst.json
  • registry.json
  • registry/blocks/hero/radial-burst/component.tsx
  • registry/components/hero-radial-burst.json
✅ Files skipped from review due to trivial changes (1)
  • apps/www/public/registry/changelogs.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/www/components/ui/hero-radial-burst.tsx
  • apps/www/public/r/hero-radial-burst.json
  • registry/components/hero-radial-burst.json
  • apps/www/public/registry/hero-radial-burst.json
  • registry/blocks/hero/radial-burst/component.tsx
  • registry.json
👮 Files not reviewed due to content moderation or server errors (1)
  • apps/www/public/registry.json

📝 Walkthrough

Walkthrough

Adds RadialBurst (canvas) and RadialBurstHero (composed section) with six time-of-day themes, a ThemeSwitcher dropdown, palette crossfading, pointer-reactive ray behavior (toggleable via burstProps.interactive), reduced-motion fallbacks, and registry/www metadata, demos, and changelogs. The hero no longer includes any stats/count-up UI.

Changes

Radial Burst Hero Component

Layer / File(s) Summary
Theme system & types
apps/www/components/ui/hero-radial-burst.tsx, registry/blocks/hero/radial-burst/component.tsx, registry/components/hero-radial-burst.json
Defines RadialBurstThemeId, RADIAL_BURST_THEMES, theme lookup, and palette RGB/gradient shapes used by the canvas and background crossfades.
Canvas renderer (RadialBurst)
apps/www/components/ui/hero-radial-burst.tsx, registry/blocks/hero/radial-burst/component.tsx, apps/www/public/r/hero-radial-burst.json
Canvas-based fiber/ray engine with DPR-aware sizing, ResizeObserver, requestAnimationFrame loop, per-ray growth/respawn, single tip dot rendering, pointer-reactive bend/brightness (toggleable by interactive), palette lerping, and reduced-motion static rendering.
ThemeSwitcher & hero composition
apps/www/components/ui/hero-radial-burst.tsx, registry/blocks/hero/radial-burst/component.tsx
ThemeSwitcher dropdown (icon + list, AnimatePresence/motion animations, outside pointerdown and Escape dismissal) and RadialBurstHero composition that crossfades section background gradients, masks the burst to a lower band, renders the masked RadialBurst behind the headline, and animates the headline (disabled for reduced motion).
www app config, demos, docs
apps/www/config/components.ts, apps/www/config/demos.tsx, apps/www/config/docs-scenarios.ts
Registers the component definition (hero-radial-burst) with LucideSparkle icon, adds a docs/demo entry mapping light/dark seed themes to defaultTheme, and inserts docs-scenarios metadata.
www public registry artifacts
apps/www/public/r/hero-radial-burst.json, apps/www/public/registry.json, apps/www/public/registry/hero-radial-burst.json, apps/www/public/registry/changelogs.json, apps/www/public/registry/index.json
Embeds the component implementation and cn util into public registry artifacts, registers dependencies, and records changelog entries for versions 1.0.0, 2.0.0 and 2.0.1.
Registry metadata, demos, ordering
registry/blocks/hero/radial-burst/component.tsx, registry/blocks/hero/radial-burst/demo.tsx, registry/components/hero-radial-burst.json, registry/demos/demo-key-order.json, registry/demos/shared.tsx, registry/manifest.json
Adds the block implementation, demo preview entry, component descriptor with prop schema (includes burstProps.interactive), updates demo key ordering and manifest/docs ordering, and imports the hero for shared demos.

Sequence Diagram

sequenceDiagram
  participant User
  participant RadialBurstHero
  participant ThemeSwitcher
  participant RadialBurst
  participant Canvas

  User->>RadialBurstHero: Mount (defaultTheme)
  RadialBurstHero->>RadialBurst: render(theme)
  RadialBurst->>Canvas: draw rays, dots, bloom
  User->>ThemeSwitcher: open & select theme
  ThemeSwitcher->>RadialBurstHero: onChange(newTheme)
  RadialBurstHero->>RadialBurst: update palette target
  RadialBurst->>Canvas: crossfade colors / animate
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • pras75299/uniqueui#39: Related registry/component generation infra previously adding new component metadata entries.

Poem

🐰
I stitched the dawn into a canvas light,
Rays that hum and softly bend by sight,
From pre-dawn hush to velvet night,
A switcher spins the colors bright,
A rabbit cheers the hero's flight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(registry): add hero-radial-burst block with time-of-day themes' accurately reflects the main change: introducing a new registry block for an animated hero with six time-of-day theme options.
Description check ✅ Passed The description covers all required template sections: Type (New component/block), Summary, Test plan with verification steps, and complete New component checklist with all items marked complete.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hero-radial-burst

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/www/components/ui/hero-radial-burst.tsx`:
- Around line 562-578: The effect in CountUp uses the local formatter function
fmt (which closes over prefix and suffix) but those props are not listed in the
useEffect dependency array, so changes to prefix/suffix won't update
mid-animation; update the dependency list for the useEffect that references fmt
(the effect which reads ref.current and calls animate/controls.stop()) to
include prefix and suffix (or alternatively memoize/derive fmt with
useCallback/useMemo tied to prefix/suffix) so the formatter is up-to-date during
animations.

In `@apps/www/public/r/hero-radial-burst.json`:
- Line 16: ThemeSwitcher currently advertises listbox semantics but implements a
simple popover of buttons; remove the incorrect listbox semantics: delete
role="listbox" from the motion.ul and remove role="option" and aria-selected
from the inner buttons (keep the existing aria-haspopup/aria-expanded on the
trigger button so AT can still see it's a popup). This keeps the widget as a
plain button menu until/unless you implement full roving-focus and
ArrowUp/ArrowDown/Home/End handling for a real listbox.
- Line 16: The CountUp component's effect doesn't include prefix and suffix in
its dependency array so the displayed text won't update when those props change;
update the useEffect dependencies in CountUp to include prefix and suffix (in
addition to value, decimals, reduced) so fmt() is re-run and the
animated/instant render reflects changes to prefix/suffix; locate CountUp, its
fmt helper and the useEffect where animate(...) is started and add prefix and
suffix to that effect's dependency list.

In `@apps/www/public/registry.json`:
- Around line 4115-4118: The CountUp component currently outputs fmt(value) on
first render causing an initial flash of the final number; change its initial
rendered content to the start value (zero) when not reduced so the animation
progresses forward from 0 to value. Concretely, in CountUp update the span's
initial children to be reduced ? fmt(value) : fmt(0) (or otherwise render fmt(0)
when reduced is false), and keep the existing useEffect that animates from 0 →
value via animate; reference CountUp, ref, fmt, useReducedMotion and the animate
call to locate and modify the JSX/initial render only.
- Around line 4115-4118: The ThemeSwitcher uses listbox/option semantics but its
items are plain tabbable buttons without listbox keyboard handling; either
implement proper listbox keyboard focus/arrow handling for ThemeSwitcher or
change the semantics to a simple popup menu. Replace the listbox/option roles in
ThemeSwitcher (the motion.ul and each item button) with menu/menuitem semantics
(or remove listbox/option and aria-selected) and ensure the trigger button keeps
aria-haspopup and aria-expanded; update references in the component named
ThemeSwitcher, the motion.ul render block, and the item buttons that currently
set role="option" and aria-selected so ARIA matches actual keyboard behavior.

In `@apps/www/public/registry/hero-radial-burst.json`:
- Line 12: The ThemeSwitcher uses a motion.ul with role="listbox" but does not
move focus into the popup or implement arrow-key/active-option handling; update
ThemeSwitcher to either use a proper menu/radio pattern or fully implement
listbox keyboard behavior: when opening (setOpen true) move focus into the list
(focus first or selected item) and manage active item with keyboard handlers
(ArrowUp/ArrowDown/Home/End to change active, Enter/Space to select) using
focusable buttons or roving tabindex and aria-selected/aria-activedescendant;
ensure each option has correct role (e.g., role="option" or
role="menuitemradio"), that ThemeSwitcher root manages focus trapping/closing on
Escape/pointer outside, and that setOpen(false) restores focus to the trigger
button (the button in ThemeSwitcher) to preserve keyboard/AT expectations.
- Around line 11-18: The component imports cn from "`@/lib/utils`" but the bundled
util is utils/cn.ts exporting function cn, so update the import used in
blocks/hero/radial-burst/component.tsx (the line importing "`@/lib/utils`") to
reference the shipped module (the utils/cn.ts export) or alternatively add a
re-export that maps "`@/lib/utils`" to the bundled utils; ensure the symbol cn is
imported from the shipped module so the component uses the exported function cn
that actually exists in the artifact.

In `@registry.json`:
- Line 4116: ThemeSwitcher currently exposes a faux listbox (motion.ul has
role="listbox" and each item button has role="option" and aria-selected) but
doesn't implement listbox keyboard/focus behavior; fix by removing the
listbox/option semantics and related aria-selected attributes so the popup is a
simple popover of plain buttons (edit ThemeSwitcher: remove role="listbox" from
the motion.ul and remove role="option" and aria-selected from the item buttons),
keeping the existing pointer/escape handlers and the trigger's
aria-haspopup/aria-expanded to preserve accessibility.
- Line 4116: The RadialBurst canvas is decorative but not reliably hidden from
assistive tech; update the returned <canvas> in the RadialBurst component (the
element rendered by the RadialBurst function using canvasRef) to include an
explicit aria-hidden="true" attribute (do not move aria-hidden to any parent
wrapper or other readable content), ensuring the decorative canvas is removed
from the accessibility tree while keeping all visible content accessible.

In `@registry/blocks/hero/radial-burst/component.tsx`:
- Around line 562-578: The useEffect for updating the counter (the effect that
references ref, reduced, value, decimals and calls fmt) omits fmt's captured
props (prefix and suffix), causing stale formatting if those props change
mid-animation; fix by ensuring the effect depends on fmt (or the underlying
props) — e.g., include fmt or prefix and suffix in the dependency array of the
useEffect that contains the animate call (the effect using ref.current, reduced,
animate, and node.textContent) or alternatively memoize fmt with
useCallback/useMemo and add that memoized fmt to the dependencies so formatting
updates correctly during animation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb9464d5-a167-4bec-a090-2e547cb269f6

📥 Commits

Reviewing files that changed from the base of the PR and between 3d87a51 and eaf912f.

📒 Files selected for processing (17)
  • apps/www/components/ui/hero-radial-burst.tsx
  • apps/www/config/components.ts
  • apps/www/config/demos.tsx
  • apps/www/config/docs-scenarios.ts
  • apps/www/public/r/hero-radial-burst.json
  • apps/www/public/r/registry.json
  • apps/www/public/registry.json
  • apps/www/public/registry/changelogs.json
  • apps/www/public/registry/hero-radial-burst.json
  • apps/www/public/registry/index.json
  • registry.json
  • registry/blocks/hero/radial-burst/component.tsx
  • registry/blocks/hero/radial-burst/demo.tsx
  • registry/components/hero-radial-burst.json
  • registry/demos/demo-key-order.json
  • registry/demos/shared.tsx
  • registry/manifest.json

Comment thread apps/www/components/ui/hero-radial-burst.tsx Outdated
Comment thread apps/www/public/r/hero-radial-burst.json Outdated
Comment thread apps/www/public/registry.json Outdated
Comment on lines +11 to +18
"path": "blocks/hero/radial-burst/component.tsx",
"content": "\"use client\";\n\nimport {\n useEffect,\n useRef,\n useState,\n type ComponentProps,\n type ReactNode,\n} from \"react\";\nimport {\n motion,\n AnimatePresence,\n animate,\n useMotionValue,\n useReducedMotion,\n type Variants,\n} from \"motion/react\";\nimport {\n CloudMoon,\n Sunrise,\n Sun,\n SunDim,\n Sunset,\n Moon,\n type LucideIcon,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ------------------------------------------------------------------ *\n * Themes — a day cycle. Each palette drives the background gradient,\n * the canvas burst colors, and (via `mode`) the text/UI contrast.\n * ------------------------------------------------------------------ */\n\nexport type RadialBurstThemeId =\n | \"pre-dawn\"\n | \"sunrise\"\n | \"daytime\"\n | \"dusk\"\n | \"sunset\"\n | \"night\";\n\ntype RGB = [number, number, number];\n\ntype Palette = {\n mode: \"light\" | \"dark\";\n /** CSS background for the section (crossfaded on theme change). */\n bg: string;\n /** Central bloom color. */\n core: RGB;\n /** Streamline color near the origin (bright) … */\n rayBase: RGB;\n /** … fading to this color at the tip. */\n rayTip: RGB;\n /** Dot color near the origin … */\n dotBase: RGB;\n /** … to this color at the tip. */\n dotTip: RGB;\n};\n\ntype ThemeDef = {\n id: RadialBurstThemeId;\n label: string;\n Icon: LucideIcon;\n palette: Palette;\n};\n\nexport const RADIAL_BURST_THEMES: ThemeDef[] = [\n {\n id: \"pre-dawn\",\n label: \"Pre-dawn\",\n Icon: CloudMoon,\n palette: {\n mode: \"dark\",\n bg: \"radial-gradient(125% 90% at 50% 102%, #3730a3 0%, #221d63 40%, #100e2e 74%, #07061a 100%)\",\n core: [165, 180, 252],\n rayBase: [199, 210, 254],\n rayTip: [99, 102, 241],\n dotBase: [199, 210, 254],\n dotTip: [129, 140, 248],\n },\n },\n {\n id: \"sunrise\",\n label: \"Sunrise\",\n Icon: Sunrise,\n palette: {\n mode: \"light\",\n bg: \"radial-gradient(125% 92% at 50% 102%, #bfdbfe 0%, #dbeafe 40%, #eff6ff 72%, #f8fafc 100%)\",\n core: [147, 197, 253],\n rayBase: [37, 99, 235],\n rayTip: [96, 165, 250],\n dotBase: [29, 78, 216],\n dotTip: [59, 130, 246],\n },\n },\n {\n id: \"daytime\",\n label: \"Daytime\",\n Icon: Sun,\n palette: {\n mode: \"light\",\n bg: \"radial-gradient(125% 92% at 50% 102%, #e9d5ff 0%, #f3e8ff 42%, #faf5ff 74%, #fcfcfe 100%)\",\n core: [216, 180, 254],\n rayBase: [124, 58, 237],\n rayTip: [219, 39, 119],\n dotBase: [30, 64, 175],\n dotTip: [219, 39, 119],\n },\n },\n {\n id: \"dusk\",\n label: \"Dusk\",\n Icon: SunDim,\n palette: {\n mode: \"light\",\n bg: \"radial-gradient(125% 92% at 50% 102%, #c4b5fd 0%, #ddd6fe 42%, #ece9fe 74%, #f6f4ff 100%)\",\n core: [167, 139, 250],\n rayBase: [109, 40, 217],\n rayTip: [236, 72, 153],\n dotBase: [76, 29, 149],\n dotTip: [219, 39, 119],\n },\n },\n {\n id: \"sunset\",\n label: \"Sunset\",\n Icon: Sunset,\n palette: {\n mode: \"light\",\n bg: \"radial-gradient(125% 92% at 50% 102%, #fed7aa 0%, #fde4cf 38%, #fff3e6 70%, #fffaf4 100%)\",\n core: [253, 186, 116],\n rayBase: [244, 63, 94],\n rayTip: [251, 146, 60],\n dotBase: [99, 102, 241],\n dotTip: [236, 72, 153],\n },\n },\n {\n id: \"night\",\n label: \"Night\",\n Icon: Moon,\n palette: {\n mode: \"dark\",\n bg: \"radial-gradient(125% 92% at 50% 102%, #4f46e5 0%, #312e81 38%, #1e1b4b 70%, #14132e 100%)\",\n core: [224, 231, 255],\n rayBase: [237, 233, 254],\n rayTip: [129, 140, 248],\n dotBase: [224, 231, 255],\n dotTip: [165, 180, 252],\n },\n },\n];\n\nconst THEME_BY_ID = Object.fromEntries(\n RADIAL_BURST_THEMES.map((t) => [t.id, t]),\n) as Record<RadialBurstThemeId, ThemeDef>;\n\n/* ------------------------------------------------------------------ *\n * Small helpers\n * ------------------------------------------------------------------ */\n\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\nconst lerpRGB = (a: RGB, b: RGB, t: number): RGB => [\n lerp(a[0], b[0], t),\n lerp(a[1], b[1], t),\n lerp(a[2], b[2], t),\n];\nconst rgba = (c: RGB, a: number) =>\n `rgba(${c[0] | 0}, ${c[1] | 0}, ${c[2] | 0}, ${a})`;\nconst easeOut = (t: number) => 1 - Math.pow(1 - t, 3);\n\ntype Ray = {\n angle: number;\n length: number; // fraction of maxRadius\n reveal: number; // 0..1 stagger delay\n phase: number; // twinkle offset\n dots: { p: number; r: number; phase: number }[];\n};\n\n/* ------------------------------------------------------------------ *\n * RadialBurst — the canvas background (reusable on its own).\n * ------------------------------------------------------------------ */\n\nexport type RadialBurstProps = {\n className?: string;\n /** Palette id. */\n theme?: RadialBurstThemeId;\n /** Ray-count multiplier (0.4–2). */\n density?: number;\n};\n\nexport function RadialBurst({\n className,\n theme = \"night\",\n density = 1,\n}: RadialBurstProps) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const reduced = useReducedMotion();\n\n // Reveal progress driven by Motion — read inside the canvas rAF loop.\n const progress = useMotionValue(reduced ? 1 : 0);\n\n // Target palette + a smoothed \"displayed\" palette so theme switches lerp.\n const targetRef = useRef<Palette>(THEME_BY_ID[theme].palette);\n const dispRef = useRef<{\n core: RGB;\n rayBase: RGB;\n rayTip: RGB;\n dotBase: RGB;\n dotTip: RGB;\n }>({\n core: [...THEME_BY_ID[theme].palette.core] as RGB,\n rayBase: [...THEME_BY_ID[theme].palette.rayBase] as RGB,\n rayTip: [...THEME_BY_ID[theme].palette.rayTip] as RGB,\n dotBase: [...THEME_BY_ID[theme].palette.dotBase] as RGB,\n dotTip: [...THEME_BY_ID[theme].palette.dotTip] as RGB,\n });\n const renderRef = useRef<() => void>(() => {});\n\n // Mount reveal animation (Motion).\n useEffect(() => {\n if (reduced) {\n progress.set(1);\n return;\n }\n const controls = animate(progress, 1, {\n duration: 1.7,\n ease: [0.22, 1, 0.36, 1],\n });\n return () => controls.stop();\n }, [progress, reduced]);\n\n // Update the target palette when the theme changes; redraw if static.\n useEffect(() => {\n targetRef.current = THEME_BY_ID[theme].palette;\n if (reduced) {\n const d = dispRef.current;\n const p = targetRef.current;\n d.core = [...p.core] as RGB;\n d.rayBase = [...p.rayBase] as RGB;\n d.rayTip = [...p.rayTip] as RGB;\n d.dotBase = [...p.dotBase] as RGB;\n d.dotTip = [...p.dotTip] as RGB;\n renderRef.current();\n }\n }, [theme, reduced]);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n let raf = 0;\n let rays: Ray[] = [];\n let maxRadius = 0;\n let originX = 0;\n let originY = 0;\n\n const seed = () => {\n const w = canvas.clientWidth;\n const h = canvas.clientHeight;\n originX = w / 2;\n originY = h * 0.99;\n maxRadius = h * 0.94;\n const count = Math.round(\n Math.min(340, Math.max(120, w / 4.8)) *\n Math.min(2, Math.max(0.4, density)),\n );\n const aMin = -0.06 * Math.PI;\n const aMax = 1.06 * Math.PI;\n rays = Array.from({ length: count }, (_, i) => {\n const t = count > 1 ? i / (count - 1) : 0.5;\n const angle = lerp(aMin, aMax, t) + (Math.random() - 0.5) * 0.012;\n // Closeness to vertical → longer rays → a dome silhouette.\n const vert = Math.sin(Math.min(Math.PI, Math.max(0, angle)));\n const length =\n (0.34 + 0.66 * vert) * (0.78 + Math.random() * 0.3) +\n (Math.random() < 0.06 ? 0.12 : 0);\n const dotCount = 1 + Math.floor(Math.random() * 4);\n return {\n angle,\n length: Math.min(1.05, length),\n reveal: (1 - vert) * 0.42 + Math.random() * 0.12,\n phase: Math.random() * Math.PI * 2,\n dots: Array.from({ length: dotCount }, () => ({\n p: 0.2 + Math.random() * 0.8,\n r: 0.6 + Math.random() * 0.9,\n phase: Math.random() * Math.PI * 2,\n })),\n };\n });\n };\n\n const resize = () => {\n const { clientWidth, clientHeight } = canvas;\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n canvas.width = Math.round(clientWidth * dpr);\n canvas.height = Math.round(clientHeight * dpr);\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n seed();\n if (reduced) renderRef.current();\n };\n\n const render = () => {\n const w = canvas.clientWidth;\n const h = canvas.clientHeight;\n const now = performance.now() / 1000;\n const prog = progress.get();\n const disp = dispRef.current;\n const target = targetRef.current;\n\n // Ease displayed colors toward the target palette (theme crossfade).\n const k = reduced ? 1 : 0.08;\n disp.core = lerpRGB(disp.core, target.core, k);\n disp.rayBase = lerpRGB(disp.rayBase, target.rayBase, k);\n disp.rayTip = lerpRGB(disp.rayTip, target.rayTip, k);\n disp.dotBase = lerpRGB(disp.dotBase, target.dotBase, k);\n disp.dotTip = lerpRGB(disp.dotTip, target.dotTip, k);\n\n ctx.clearRect(0, 0, w, h);\n const dark = target.mode === \"dark\";\n // Dark themes glow additively; light themes paint normally.\n ctx.globalCompositeOperation = dark ? \"lighter\" : \"source-over\";\n\n // Central bloom.\n const bloomR = Math.min(w * 0.22, maxRadius * 0.5) * (0.6 + 0.4 * prog);\n const bloom = ctx.createRadialGradient(\n originX,\n originY,\n 0,\n originX,\n originY,\n bloomR,\n );\n bloom.addColorStop(0, rgba(disp.core, dark ? 0.7 : 0.6));\n bloom.addColorStop(0.45, rgba(disp.core, dark ? 0.22 : 0.18));\n bloom.addColorStop(1, rgba(disp.core, 0));\n ctx.fillStyle = bloom;\n ctx.beginPath();\n ctx.arc(originX, originY, bloomR, 0, Math.PI * 2);\n ctx.fill();\n\n ctx.lineWidth = 0.7;\n ctx.lineCap = \"round\";\n\n for (let i = 0; i < rays.length; i++) {\n const ray = rays[i];\n const lp = easeOut(\n Math.min(1, Math.max(0, (prog - ray.reveal) / (1 - 0.54))),\n );\n if (lp <= 0) continue;\n const twinkle = reduced ? 1 : 0.72 + 0.28 * Math.sin(now * 1.4 + ray.phase);\n const dx = Math.cos(ray.angle);\n const dy = -Math.sin(ray.angle);\n const len = ray.length * maxRadius * lp;\n const ex = originX + dx * len;\n const ey = originY + dy * len;\n\n const grad = ctx.createLinearGradient(originX, originY, ex, ey);\n grad.addColorStop(0, rgba(disp.rayBase, 0.0));\n grad.addColorStop(0.05, rgba(disp.rayBase, 0.98 * twinkle));\n grad.addColorStop(0.4, rgba(disp.rayBase, 0.55 * twinkle));\n grad.addColorStop(1, rgba(disp.rayTip, 0));\n ctx.strokeStyle = grad;\n ctx.beginPath();\n ctx.moveTo(originX, originY);\n ctx.lineTo(ex, ey);\n ctx.stroke();\n\n // Dots — drift slowly outward for a living \"data\" feel.\n for (let d = 0; d < ray.dots.length; d++) {\n const dot = ray.dots[d];\n const dp = reduced ? dot.p : (dot.p + now * 0.025) % 1;\n if (dp > lp) continue;\n const px = originX + dx * ray.length * maxRadius * dp;\n const py = originY + dy * ray.length * maxRadius * dp;\n const dtw = reduced ? 1 : 0.5 + 0.5 * Math.sin(now * 2 + dot.phase);\n ctx.fillStyle = rgba(\n lerpRGB(disp.dotBase, disp.dotTip, dp),\n (dark ? 0.85 : 0.9) * (0.4 + 0.6 * dtw) * (1 - dp * 0.35),\n );\n ctx.beginPath();\n ctx.arc(px, py, dot.r, 0, Math.PI * 2);\n ctx.fill();\n }\n }\n\n ctx.globalCompositeOperation = \"source-over\";\n };\n renderRef.current = render;\n\n resize();\n const ro = new ResizeObserver(resize);\n ro.observe(canvas);\n\n if (reduced) {\n render();\n return () => ro.disconnect();\n }\n\n const loop = () => {\n render();\n raf = requestAnimationFrame(loop);\n };\n raf = requestAnimationFrame(loop);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n };\n }, [density, progress, reduced]);\n\n return (\n <canvas\n ref={canvasRef}\n aria-hidden\n className={cn(\"absolute inset-0 h-full w-full\", className)}\n />\n );\n}\n\n/* ------------------------------------------------------------------ *\n * ThemeSwitcher — the corner dropdown.\n * ------------------------------------------------------------------ */\n\nfunction ThemeSwitcher({\n theme,\n onChange,\n mode,\n}: {\n theme: RadialBurstThemeId;\n onChange: (id: RadialBurstThemeId) => void;\n mode: \"light\" | \"dark\";\n}) {\n const [open, setOpen] = useState(false);\n const rootRef = useRef<HTMLDivElement>(null);\n const Current = THEME_BY_ID[theme].Icon;\n const dark = mode === \"dark\";\n\n useEffect(() => {\n if (!open) return;\n const onPointer = (e: PointerEvent) => {\n if (!rootRef.current?.contains(e.target as Node)) setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(\"pointerdown\", onPointer);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(\"pointerdown\", onPointer);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, [open]);\n\n return (\n <div ref={rootRef} className=\"relative\">\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-label={`Theme: ${THEME_BY_ID[theme].label}. Change theme`}\n className={cn(\n \"inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-lg border backdrop-blur transition-colors\",\n dark\n ? \"border-white/15 bg-white/5 text-white/80 hover:bg-white/10\"\n : \"border-slate-900/10 bg-white/70 text-slate-600 hover:bg-white\",\n )}\n >\n <Current className=\"h-4 w-4\" />\n </button>\n\n <AnimatePresence>\n {open && (\n <motion.ul\n role=\"listbox\"\n aria-label=\"Theme\"\n initial={{ opacity: 0, y: -6, scale: 0.96 }}\n animate={{ opacity: 1, y: 0, scale: 1 }}\n exit={{ opacity: 0, y: -6, scale: 0.96 }}\n transition={{ type: \"spring\", stiffness: 320, damping: 26 }}\n style={{ transformOrigin: \"top right\" }}\n className={cn(\n \"absolute right-0 top-11 z-30 w-40 overflow-hidden rounded-xl border p-1 shadow-xl backdrop-blur-md\",\n dark\n ? \"border-white/10 bg-[#1b1842]/90 text-white/80\"\n : \"border-slate-900/10 bg-white/90 text-slate-700\",\n )}\n >\n {RADIAL_BURST_THEMES.map((t) => {\n const active = t.id === theme;\n return (\n <li key={t.id}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n onClick={() => {\n onChange(t.id);\n setOpen(false);\n }}\n className={cn(\n \"flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-sm transition-colors\",\n active\n ? dark\n ? \"bg-white/10 text-white\"\n : \"bg-slate-900/5 text-slate-900\"\n : dark\n ? \"hover:bg-white/5\"\n : \"hover:bg-slate-900/5\",\n )}\n >\n <t.Icon className=\"h-4 w-4 shrink-0 opacity-80\" />\n {t.label}\n </button>\n </li>\n );\n })}\n </motion.ul>\n )}\n </AnimatePresence>\n </div>\n );\n}\n\n/* ------------------------------------------------------------------ *\n * Count-up stat (Motion-driven).\n * ------------------------------------------------------------------ */\n\ntype Stat = {\n prefix?: string;\n value: number;\n decimals?: number;\n suffix?: string;\n label: string;\n};\n\nconst DEFAULT_STATS: Stat[] = [\n { value: 135, suffix: \"+\", label: \"currencies and payment\\nmethods supported\" },\n { prefix: \"US$\", value: 1.9, decimals: 1, suffix: \"tn\", label: \"in payments volume\\nprocessed in 2025\" },\n { value: 99.999, decimals: 3, suffix: \"%\", label: \"historical uptime\\nfor Stripe services\" },\n { value: 200, suffix: \"M+\", label: \"active subscriptions\\nmanaged on Stripe Billing\" },\n];\n\nfunction CountUp({\n value,\n decimals = 0,\n prefix = \"\",\n suffix = \"\",\n}: Stat) {\n const ref = useRef<HTMLSpanElement>(null);\n const reduced = useReducedMotion();\n const fmt = (v: number) => {\n const fixed = v.toFixed(decimals);\n const [int, dec] = fixed.split(\".\");\n const withSep = int.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n return `${prefix}${dec !== undefined ? `${withSep}.${dec}` : withSep}${suffix}`;\n };\n\n useEffect(() => {\n const node = ref.current;\n if (!node) return;\n if (reduced) {\n node.textContent = fmt(value);\n return;\n }\n const controls = animate(0, value, {\n duration: 1.8,\n ease: [0.22, 1, 0.36, 1],\n onUpdate: (v) => {\n node.textContent = fmt(v);\n },\n });\n return () => controls.stop();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value, decimals, reduced]);\n\n return (\n <span ref={ref} className=\"tabular-nums\">\n {fmt(value)}\n </span>\n );\n}\n\n/* ------------------------------------------------------------------ *\n * RadialBurstHero — full composition.\n * ------------------------------------------------------------------ */\n\nconst containerVariants: Variants = {\n hidden: {},\n visible: { transition: { staggerChildren: 0.08, delayChildren: 0.1 } },\n};\nconst itemVariants: Variants = {\n hidden: { opacity: 0, y: 16 },\n visible: {\n opacity: 1,\n y: 0,\n transition: { type: \"spring\", stiffness: 140, damping: 20 },\n },\n};\n\nexport type RadialBurstHeroProps = Omit<\n ComponentProps<\"section\">,\n \"children\" | \"title\"\n> & {\n /** Initial theme; also synced if it changes (e.g. from a site toggle). */\n defaultTheme?: RadialBurstThemeId;\n title?: ReactNode;\n stats?: Stat[];\n burstProps?: Omit<RadialBurstProps, \"theme\">;\n};\n\nexport function RadialBurstHero({\n className,\n defaultTheme = \"night\",\n title = (\n <>\n The backbone\n <br />\n of global commerce\n </>\n ),\n stats = DEFAULT_STATS,\n burstProps,\n ...rest\n}: RadialBurstHeroProps) {\n const [theme, setTheme] = useState<RadialBurstThemeId>(defaultTheme);\n const reduced = useReducedMotion();\n\n // Keep in sync if the consumer (or docs theme toggle) changes defaultTheme.\n useEffect(() => {\n setTheme(defaultTheme);\n }, [defaultTheme]);\n\n const palette = THEME_BY_ID[theme].palette;\n const dark = palette.mode === \"dark\";\n\n return (\n <section\n {...rest}\n data-theme={theme}\n className={cn(\n \"relative isolate flex min-h-[100svh] w-full flex-col overflow-hidden\",\n dark ? \"text-white\" : \"text-slate-900\",\n className,\n )}\n >\n {/* Background gradient — crossfades between themes. */}\n <AnimatePresence initial={false}>\n <motion.div\n key={theme}\n aria-hidden\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.6, ease: \"easeInOut\" }}\n className=\"absolute inset-0 -z-10\"\n style={{ background: palette.bg }}\n />\n </AnimatePresence>\n\n {/* Canvas burst. */}\n <RadialBurst theme={theme} {...burstProps} />\n\n {/* Theme switcher. */}\n <div className=\"absolute right-5 top-5 z-20 sm:right-8 sm:top-8\">\n <ThemeSwitcher theme={theme} onChange={setTheme} mode={palette.mode} />\n </div>\n\n {/* Content. */}\n <motion.div\n variants={reduced ? undefined : containerVariants}\n initial={reduced ? false : \"hidden\"}\n animate={reduced ? undefined : \"visible\"}\n className=\"relative z-10 mx-auto flex w-full max-w-5xl flex-col px-6 pt-[12vh]\"\n >\n <motion.h1\n variants={reduced ? undefined : itemVariants}\n className=\"text-center text-balance text-4xl font-semibold leading-[1.05] tracking-tight sm:text-5xl md:text-6xl\"\n >\n {title}\n </motion.h1>\n\n <motion.div\n variants={reduced ? undefined : itemVariants}\n className={cn(\n \"mt-14 grid grid-cols-2 gap-y-10 border-t pt-10 md:grid-cols-4\",\n dark ? \"border-white/10\" : \"border-slate-900/10\",\n )}\n >\n {stats.map((stat, i) => (\n <div key={i} className=\"px-2 text-center md:px-4\">\n <div\n className={cn(\n \"text-3xl font-medium tracking-tight sm:text-4xl\",\n i === 0\n ? dark\n ? \"text-white\"\n : \"text-slate-900\"\n : dark\n ? \"text-white/85\"\n : \"text-slate-500\",\n )}\n >\n <CountUp {...stat} />\n </div>\n <p\n className={cn(\n \"mx-auto mt-2 max-w-[16ch] whitespace-pre-line text-xs leading-relaxed sm:text-sm\",\n dark ? \"text-white/45\" : \"text-slate-500\",\n )}\n >\n {stat.label}\n </p>\n </div>\n ))}\n </motion.div>\n </motion.div>\n </section>\n );\n}\n\nexport default RadialBurstHero;\n",
"type": "registry:ui",
"integrity": "sha384-858wzmwi4WlpfzklkxQxjFDmEhxfmWG9OTc5CbN2ZvkxlulRZPmXVWJboYrK+/ux"
},
{
"path": "utils/cn.ts",
"content": "import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the bundled registry artifact self-consistent.

blocks/hero/radial-burst/component.tsx imports cn from @/lib/utils, but this artifact ships utils/cn.ts instead. That leaves the bundled util unused and can break installs in consumers that do not already provide @/lib/utils. Point the component at the shipped util, or emit the util at the path the component actually imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/www/public/registry/hero-radial-burst.json` around lines 11 - 18, The
component imports cn from "`@/lib/utils`" but the bundled util is utils/cn.ts
exporting function cn, so update the import used in
blocks/hero/radial-burst/component.tsx (the line importing "`@/lib/utils`") to
reference the shipped module (the utils/cn.ts export) or alternatively add a
re-export that maps "`@/lib/utils`" to the bundled utils; ensure the symbol cn is
imported from the shipped module so the component uses the exported function cn
that actually exists in the artifact.

Comment thread apps/www/public/registry/hero-radial-burst.json Outdated
Comment thread registry.json Outdated
Comment thread registry/blocks/hero/radial-burst/component.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/www/public/registry.json (1)

4116-4116: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

ARIA roles still don't match the switcher behavior.

The embedded ThemeSwitcher still exposes a listbox/option relationship, but the popup behaves like a tabbable button menu. Either add real listbox focus/arrow-key handling or switch the source component to menu semantics, then regenerate this manifest.

Based on learnings: "Never edit generated manifests by hand — always run pnpm build:registry to regenerate them."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/www/public/registry.json` at line 4116, ThemeSwitcher currently exposes
listbox/option semantics but behaves like a button menu; update ThemeSwitcher to
use menu semantics: change the floating motion.ul from role="listbox" to
role="menu" (adjust aria-label accordingly) and change each item button from
role="option" to role="menuitem" (keep aria-selected removed or replace with
aria-checked if needed), ensure the trigger button still has aria-expanded and
aria-haspopup=\"menu\", and keep existing Escape/Pointer handlers; after making
these role changes in ThemeSwitcher (the motion.ul and the mapped <button> items
inside RADIAL_BURST_THEMES), run pnpm build:registry to regenerate the manifest
rather than editing it by hand.
🧹 Nitpick comments (1)
apps/www/components/ui/hero-radial-burst.tsx (1)

652-696: ⚡ Quick win

Listbox semantics declared without full keyboard support.

The dropdown uses role="listbox" and role="option" but lacks the required ArrowUp/ArrowDown/Home/End roving focus behavior. Screen readers will announce listbox interaction model but keyboard users won't get it.

Either implement full listbox keyboard navigation or simplify to a plain button menu by removing role="listbox", role="option", and aria-selected, keeping only aria-haspopup="menu" on the trigger.

Simplify to button menu (if not implementing full listbox)
           <motion.ul
-            role="listbox"
-            aria-label="Theme"
+            role="menu"
+            aria-label="Select theme"
             initial={{ opacity: 0, y: -6, scale: 0.96 }}
             ...
           >
             {RADIAL_BURST_THEMES.map((t) => {
               const active = t.id === theme;
               return (
-                <li key={t.id}>
+                <li key={t.id} role="none">
                   <button
                     type="button"
-                    role="option"
-                    aria-selected={active}
+                    role="menuitem"
+                    aria-current={active ? "true" : undefined}
                     onClick={() => {

Also update the trigger:

-        aria-haspopup="listbox"
+        aria-haspopup="menu"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/www/components/ui/hero-radial-burst.tsx` around lines 652 - 696, The
list uses listbox semantics without keyboard roving — remove the incorrect ARIA
roles and attributes: drop role="listbox" from motion.ul and remove
role="option" and aria-selected from each button rendered in the
RADIAL_BURST_THEMES map (keep the existing onClick, setOpen, onChange, and
visual classes intact), and treat the items as a simple button menu; then update
the dropdown trigger (the element that toggles setOpen) to include
aria-haspopup="menu" and a corresponding aria-expanded toggle so assistive tech
knows it's a menu.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@registry.json`:
- Line 4116: RadialBurstHero is missing the count-up stat row promised in the PR
(the hero now only renders the headline); restore the removed stats block by
reintroducing the stat row JSX into the RadialBurstHero return (between the
headline motion.h1 and the section close), using the same structure/semantics as
the original design (a container with numeric counters and labels), wire it to
any existing props or local constants if needed, and ensure styles/aria match
surrounding elements; look for the RadialBurstHero function and the place where
title is rendered (motion.h1) to locate where to insert the stat row and update
any exports/props (RadialBurstHeroProps / burstProps) if the stats require
external data.

---

Duplicate comments:
In `@apps/www/public/registry.json`:
- Line 4116: ThemeSwitcher currently exposes listbox/option semantics but
behaves like a button menu; update ThemeSwitcher to use menu semantics: change
the floating motion.ul from role="listbox" to role="menu" (adjust aria-label
accordingly) and change each item button from role="option" to role="menuitem"
(keep aria-selected removed or replace with aria-checked if needed), ensure the
trigger button still has aria-expanded and aria-haspopup=\"menu\", and keep
existing Escape/Pointer handlers; after making these role changes in
ThemeSwitcher (the motion.ul and the mapped <button> items inside
RADIAL_BURST_THEMES), run pnpm build:registry to regenerate the manifest rather
than editing it by hand.

---

Nitpick comments:
In `@apps/www/components/ui/hero-radial-burst.tsx`:
- Around line 652-696: The list uses listbox semantics without keyboard roving —
remove the incorrect ARIA roles and attributes: drop role="listbox" from
motion.ul and remove role="option" and aria-selected from each button rendered
in the RADIAL_BURST_THEMES map (keep the existing onClick, setOpen, onChange,
and visual classes intact), and treat the items as a simple button menu; then
update the dropdown trigger (the element that toggles setOpen) to include
aria-haspopup="menu" and a corresponding aria-expanded toggle so assistive tech
knows it's a menu.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a5b3179-fff2-4814-918f-d95ad43cdc9e

📥 Commits

Reviewing files that changed from the base of the PR and between eaf912f and 48781de.

📒 Files selected for processing (13)
  • apps/www/components/ui/hero-radial-burst.tsx
  • apps/www/config/components.ts
  • apps/www/config/demos.tsx
  • apps/www/config/docs-scenarios.ts
  • apps/www/public/r/hero-radial-burst.json
  • apps/www/public/r/registry.json
  • apps/www/public/registry.json
  • apps/www/public/registry/changelogs.json
  • apps/www/public/registry/hero-radial-burst.json
  • registry.json
  • registry/blocks/hero/radial-burst/component.tsx
  • registry/blocks/hero/radial-burst/demo.tsx
  • registry/components/hero-radial-burst.json
✅ Files skipped from review due to trivial changes (3)
  • apps/www/config/components.ts
  • apps/www/config/demos.tsx
  • apps/www/public/r/registry.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • registry/blocks/hero/radial-burst/demo.tsx
  • apps/www/config/docs-scenarios.ts

Comment thread registry.json Outdated
"files": [
{
"path": "blocks/hero/radial-burst/component.tsx",
"content": "\"use client\";\n\nimport {\n useEffect,\n useRef,\n useState,\n type ComponentProps,\n type ReactNode,\n} from \"react\";\nimport {\n motion,\n AnimatePresence,\n animate,\n useMotionValue,\n useReducedMotion,\n} from \"motion/react\";\nimport {\n CloudMoon,\n Sunrise,\n Sun,\n SunDim,\n Sunset,\n Moon,\n type LucideIcon,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ------------------------------------------------------------------ *\n * Themes — a day cycle. Each palette drives the background gradient,\n * the canvas burst colors, and (via `mode`) the text/UI contrast.\n * ------------------------------------------------------------------ */\n\nexport type RadialBurstThemeId =\n | \"pre-dawn\"\n | \"sunrise\"\n | \"daytime\"\n | \"dusk\"\n | \"sunset\"\n | \"night\";\n\ntype RGB = [number, number, number];\n\ntype Palette = {\n mode: \"light\" | \"dark\";\n /** CSS background for the section (crossfaded on theme change). */\n bg: string;\n /** Central bloom color. */\n core: RGB;\n /** Streamline color near the origin (bright) … */\n rayBase: RGB;\n /** … fading to this color at the tip. */\n rayTip: RGB;\n /** Dot color near the origin … */\n dotBase: RGB;\n /** … to this color at the tip. */\n dotTip: RGB;\n};\n\ntype ThemeDef = {\n id: RadialBurstThemeId;\n label: string;\n Icon: LucideIcon;\n palette: Palette;\n};\n\nexport const RADIAL_BURST_THEMES: ThemeDef[] = [\n {\n id: \"pre-dawn\",\n label: \"Pre-dawn\",\n Icon: CloudMoon,\n palette: {\n mode: \"dark\",\n bg: \"radial-gradient(125% 90% at 50% 102%, #3730a3 0%, #221d63 40%, #100e2e 74%, #07061a 100%)\",\n core: [165, 180, 252],\n rayBase: [199, 210, 254],\n rayTip: [99, 102, 241],\n dotBase: [199, 210, 254],\n dotTip: [129, 140, 248],\n },\n },\n {\n id: \"sunrise\",\n label: \"Sunrise\",\n Icon: Sunrise,\n palette: {\n mode: \"light\",\n bg: \"radial-gradient(125% 92% at 50% 102%, #bfdbfe 0%, #dbeafe 40%, #eff6ff 72%, #f8fafc 100%)\",\n core: [147, 197, 253],\n rayBase: [37, 99, 235],\n rayTip: [96, 165, 250],\n dotBase: [29, 78, 216],\n dotTip: [59, 130, 246],\n },\n },\n {\n id: \"daytime\",\n label: \"Daytime\",\n Icon: Sun,\n palette: {\n mode: \"light\",\n bg: \"radial-gradient(125% 92% at 50% 102%, #e9d5ff 0%, #f3e8ff 42%, #faf5ff 74%, #fcfcfe 100%)\",\n core: [216, 180, 254],\n rayBase: [124, 58, 237],\n rayTip: [219, 39, 119],\n dotBase: [30, 64, 175],\n dotTip: [219, 39, 119],\n },\n },\n {\n id: \"dusk\",\n label: \"Dusk\",\n Icon: SunDim,\n palette: {\n mode: \"light\",\n bg: \"radial-gradient(125% 92% at 50% 102%, #c4b5fd 0%, #ddd6fe 42%, #ece9fe 74%, #f6f4ff 100%)\",\n core: [167, 139, 250],\n rayBase: [109, 40, 217],\n rayTip: [236, 72, 153],\n dotBase: [76, 29, 149],\n dotTip: [219, 39, 119],\n },\n },\n {\n id: \"sunset\",\n label: \"Sunset\",\n Icon: Sunset,\n palette: {\n mode: \"light\",\n bg: \"radial-gradient(125% 92% at 50% 102%, #fed7aa 0%, #fde4cf 38%, #fff3e6 70%, #fffaf4 100%)\",\n core: [253, 186, 116],\n rayBase: [244, 63, 94],\n rayTip: [251, 146, 60],\n dotBase: [99, 102, 241],\n dotTip: [236, 72, 153],\n },\n },\n {\n id: \"night\",\n label: \"Night\",\n Icon: Moon,\n palette: {\n mode: \"dark\",\n bg: \"radial-gradient(125% 92% at 50% 102%, #4f46e5 0%, #312e81 38%, #1e1b4b 70%, #14132e 100%)\",\n core: [224, 231, 255],\n rayBase: [237, 233, 254],\n rayTip: [129, 140, 248],\n dotBase: [224, 231, 255],\n dotTip: [165, 180, 252],\n },\n },\n];\n\nconst THEME_BY_ID = Object.fromEntries(\n RADIAL_BURST_THEMES.map((t) => [t.id, t]),\n) as Record<RadialBurstThemeId, ThemeDef>;\n\n/* ------------------------------------------------------------------ *\n * Small helpers\n * ------------------------------------------------------------------ */\n\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\nconst lerpRGB = (a: RGB, b: RGB, t: number): RGB => [\n lerp(a[0], b[0], t),\n lerp(a[1], b[1], t),\n lerp(a[2], b[2], t),\n];\nconst rgba = (c: RGB, a: number) =>\n `rgba(${c[0] | 0}, ${c[1] | 0}, ${c[2] | 0}, ${a})`;\nconst clamp = (v: number, a: number, b: number) => Math.min(b, Math.max(a, v));\nconst easeOut = (t: number) => 1 - Math.pow(1 - t, 3);\n/** Wrap an angle into (-π, π]. */\nconst wrapAngle = (a: number) => {\n let x = a;\n while (x > Math.PI) x -= 2 * Math.PI;\n while (x < -Math.PI) x += 2 * Math.PI;\n return x;\n};\n/** Distance from point (px,py) to segment (ax,ay)-(bx,by). */\nconst segDist = (\n px: number,\n py: number,\n ax: number,\n ay: number,\n bx: number,\n by: number,\n) => {\n const dx = bx - ax;\n const dy = by - ay;\n const len2 = dx * dx + dy * dy || 1;\n const t = clamp(((px - ax) * dx + (py - ay) * dy) / len2, 0, 1);\n const cx = ax + t * dx;\n const cy = ay + t * dy;\n return Math.hypot(px - cx, py - cy);\n};\n\n/** Horizontal spread multiplier — widens the fan without changing its height. */\nconst SPREAD_X = 1.3;\n\n/** One fiber: a streamline that grows, holds, extends, fades, then respawns. */\ntype Ray = {\n angle: number; // base emission angle (radians)\n maxLen: number; // target length as a fraction of maxRadius\n speed: number; // life units per second (→ lifetime ≈ 1/speed)\n life: number; // < 0 staggered delay, 0..1 active\n width: number; // core stroke width\n bright: number; // base brightness 0..1\n phase: number; // twinkle offset\n react: number; // smoothed pointer reaction 0..1\n bend: number; // smoothed angular bend toward the pointer\n hasDot: boolean; // whether a glowing dot rides this fiber's tip\n dotR: number; // tip-dot radius\n dotPhase: number; // tip-dot twinkle offset\n};\n\n/* ------------------------------------------------------------------ *\n * RadialBurst — the interactive canvas background (reusable on its own).\n *\n * Fibers stream out of a bottom-center origin in a wide radial fan. Each\n * one continuously grows, slightly over-extends, fades, and regenerates\n * with fresh angle/length/speed/opacity, while glowing dots travel along\n * it. Rays near the pointer brighten, stretch, and bend toward it, then\n * ease back to their drift. The burst stays in the lower band so it never\n * reaches the headline above.\n * ------------------------------------------------------------------ */\n\nexport type RadialBurstProps = {\n className?: string;\n /** Palette id. */\n theme?: RadialBurstThemeId;\n /** Ray-count multiplier (0.4–2). */\n density?: number;\n /** Disable pointer reactivity. */\n interactive?: boolean;\n};\n\nexport function RadialBurst({\n className,\n theme = \"night\",\n density = 1,\n interactive = true,\n}: RadialBurstProps) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const reduced = useReducedMotion();\n\n // Global intro fade, driven by Motion — read inside the canvas rAF loop.\n const intro = useMotionValue(reduced ? 1 : 0);\n\n // Target palette + a smoothed \"displayed\" palette so theme switches lerp.\n const targetRef = useRef<Palette>(THEME_BY_ID[theme].palette);\n const dispRef = useRef<{\n core: RGB;\n rayBase: RGB;\n rayTip: RGB;\n dotBase: RGB;\n dotTip: RGB;\n }>({\n core: [...THEME_BY_ID[theme].palette.core] as RGB,\n rayBase: [...THEME_BY_ID[theme].palette.rayBase] as RGB,\n rayTip: [...THEME_BY_ID[theme].palette.rayTip] as RGB,\n dotBase: [...THEME_BY_ID[theme].palette.dotBase] as RGB,\n dotTip: [...THEME_BY_ID[theme].palette.dotTip] as RGB,\n });\n const renderRef = useRef<(dt: number) => void>(() => {});\n\n // Mount fade-in (Motion).\n useEffect(() => {\n if (reduced) {\n intro.set(1);\n return;\n }\n const controls = animate(intro, 1, {\n duration: 1.6,\n ease: [0.22, 1, 0.36, 1],\n });\n return () => controls.stop();\n }, [intro, reduced]);\n\n // Update the target palette when the theme changes; redraw if static.\n useEffect(() => {\n targetRef.current = THEME_BY_ID[theme].palette;\n if (reduced) {\n const d = dispRef.current;\n const p = targetRef.current;\n d.core = [...p.core] as RGB;\n d.rayBase = [...p.rayBase] as RGB;\n d.rayTip = [...p.rayTip] as RGB;\n d.dotBase = [...p.dotBase] as RGB;\n d.dotTip = [...p.dotTip] as RGB;\n renderRef.current(0);\n }\n }, [theme, reduced]);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n let raf = 0;\n let rays: Ray[] = [];\n let maxRadius = 0;\n let originX = 0;\n let originY = 0;\n\n const pointer = { x: 0, y: 0, active: false };\n\n const respawn = (ray: Ray, initial: boolean) => {\n const aMin = -0.06 * Math.PI;\n const aMax = 1.06 * Math.PI;\n const angle = lerp(aMin, aMax, Math.random()) + (Math.random() - 0.5) * 0.05;\n // Mild bias toward vertical, but side rays stay long so the fan fills\n // the full width along the bottom rather than tapering to a dome.\n const vert = Math.sin(clamp(angle, 0, Math.PI));\n ray.angle = angle;\n ray.maxLen = Math.min(\n 1.05,\n (0.8 + 0.2 * vert) * (0.6 + Math.random() * 0.45) +\n (Math.random() < 0.06 ? 0.12 : 0),\n );\n ray.speed = 0.085 + Math.random() * 0.13; // lifetime ≈ 4.5–11.8s\n ray.life = initial ? Math.random() : -Math.random() * 0.6;\n ray.width = 0.55 + Math.random() * 0.5;\n ray.bright = 0.5 + Math.random() * 0.5;\n ray.phase = Math.random() * Math.PI * 2;\n // A single glowing dot sits at the tip — it rides the growing tip, it\n // does not travel along the fiber.\n ray.hasDot = Math.random() < 0.72;\n ray.dotR = 0.8 + Math.random() * 1;\n ray.dotPhase = Math.random() * Math.PI * 2;\n };\n\n const seed = () => {\n const w = canvas.clientWidth;\n const h = canvas.clientHeight;\n originX = w / 2;\n // Origin sits on the bottom edge so the burst touches the bottom.\n originY = h;\n // Lower-band height — kept well below the headline, ~100px shorter.\n maxRadius = Math.max(h * 0.4, h * 0.6 - 100);\n const count = Math.round(\n Math.min(400, Math.max(220, w / 3.2)) * clamp(density, 0.4, 2),\n );\n rays = Array.from({ length: count }, () => {\n const ray: Ray = {\n angle: 0,\n maxLen: 0,\n speed: 0,\n life: 0,\n width: 1,\n bright: 1,\n phase: 0,\n react: 0,\n bend: 0,\n hasDot: false,\n dotR: 1,\n dotPhase: 0,\n };\n respawn(ray, true);\n return ray;\n });\n };\n\n const resize = () => {\n const { clientWidth, clientHeight } = canvas;\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n canvas.width = Math.round(clientWidth * dpr);\n canvas.height = Math.round(clientHeight * dpr);\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n seed();\n if (reduced) renderRef.current(0);\n };\n\n const render = (dt: number) => {\n const w = canvas.clientWidth;\n const h = canvas.clientHeight;\n const now = performance.now() / 1000;\n const introA = intro.get();\n const disp = dispRef.current;\n const target = targetRef.current;\n\n // Ease displayed colors toward the target palette (theme crossfade).\n const k = reduced ? 1 : 0.08;\n disp.core = lerpRGB(disp.core, target.core, k);\n disp.rayBase = lerpRGB(disp.rayBase, target.rayBase, k);\n disp.rayTip = lerpRGB(disp.rayTip, target.rayTip, k);\n disp.dotBase = lerpRGB(disp.dotBase, target.dotBase, k);\n disp.dotTip = lerpRGB(disp.dotTip, target.dotTip, k);\n\n ctx.clearRect(0, 0, w, h);\n const dark = target.mode === \"dark\";\n // Dark themes glow additively; light themes paint normally.\n ctx.globalCompositeOperation = dark ? \"lighter\" : \"source-over\";\n ctx.lineCap = \"round\";\n\n // Central bloom — a soft, diffuse glow rather than a hard bright disc.\n const bloomR =\n Math.min(w * 0.22, maxRadius * 0.6) * (0.7 + 0.3 * introA);\n const bloom = ctx.createRadialGradient(\n originX,\n originY,\n 0,\n originX,\n originY,\n bloomR,\n );\n bloom.addColorStop(0, rgba(disp.core, (dark ? 0.5 : 0.44) * introA));\n bloom.addColorStop(0.3, rgba(disp.core, (dark ? 0.18 : 0.15) * introA));\n bloom.addColorStop(0.65, rgba(disp.core, (dark ? 0.06 : 0.05) * introA));\n bloom.addColorStop(1, rgba(disp.core, 0));\n ctx.fillStyle = bloom;\n ctx.beginPath();\n ctx.arc(originX, originY, bloomR, 0, Math.PI * 2);\n ctx.fill();\n\n const pointerOn = interactive && !reduced && pointer.active;\n const reactR = 170; // px radius of pointer influence\n // No reaction near the origin — only the middle/tip of fibers respond.\n const originGuard = maxRadius * 0.22;\n const pointerNearOrigin =\n Math.hypot(pointer.x - originX, pointer.y - originY) < originGuard;\n\n for (let i = 0; i < rays.length; i++) {\n const ray = rays[i];\n\n if (!reduced) {\n ray.life += ray.speed * dt;\n if (ray.life >= 1) respawn(ray, false);\n }\n if (ray.life < 0) continue;\n\n const life = reduced ? 0.62 : ray.life;\n const growT = clamp(life / 0.7, 0, 1);\n const lenFrac = easeOut(growT);\n const extend = life > 0.7 ? (life - 0.7) / 0.3 : 0;\n const env = reduced\n ? 1\n : Math.min(1, life / 0.12) *\n (life > 0.8 ? clamp(1 - (life - 0.8) / 0.2, 0, 1) : 1);\n if (env <= 0) continue;\n\n const baseLen =\n ray.maxLen * maxRadius * lenFrac * (1 + 0.06 * extend);\n\n // Pointer reaction — engage quickly, return slowly. Hit-test only the\n // outer 35%→tip span so the dense near-origin zone stays calm.\n if (pointerOn && !pointerNearOrigin) {\n const cx = Math.cos(ray.angle) * SPREAD_X;\n const cy = -Math.sin(ray.angle);\n const ix = originX + cx * baseLen * 0.35;\n const iy = originY + cy * baseLen * 0.35;\n const bx = originX + cx * baseLen;\n const by = originY + cy * baseLen;\n const d = segDist(pointer.x, pointer.y, ix, iy, bx, by);\n let reactTarget = 0;\n let bendTarget = 0;\n if (d < reactR) {\n reactTarget = 1 - d / reactR;\n reactTarget *= reactTarget;\n const pAng = Math.atan2(\n -(pointer.y - originY),\n (pointer.x - originX) / SPREAD_X,\n );\n bendTarget = clamp(wrapAngle(pAng - ray.angle), -0.4, 0.4);\n }\n ray.react +=\n (reactTarget - ray.react) * (reactTarget > ray.react ? 0.14 : 0.06);\n ray.bend += (bendTarget * ray.react - ray.bend) * 0.1;\n } else if (ray.react !== 0 || ray.bend !== 0) {\n ray.react += -ray.react * 0.06;\n ray.bend += -ray.bend * 0.1;\n }\n\n const react = ray.react;\n const drawAngle = ray.angle + ray.bend;\n const dirx = Math.cos(drawAngle) * SPREAD_X;\n const diry = -Math.sin(drawAngle);\n const effLen = baseLen * (1 + 0.12 * react);\n const ex = originX + dirx * effLen;\n const ey = originY + diry * effLen;\n\n const twinkle = reduced ? 1 : 0.82 + 0.18 * Math.sin(now * 1.3 + ray.phase);\n const aBase = clamp(\n env * introA * ray.bright * twinkle * (1 + 0.9 * react),\n 0,\n 1,\n );\n\n // Brightness builds up *along* the fiber: near-zero through the dense\n // convergence zone at the origin (so overlapping starts don't blow out\n // to white), peaking once the fibers have fanned apart, fading at the tip.\n const grad = ctx.createLinearGradient(originX, originY, ex, ey);\n grad.addColorStop(0, rgba(disp.rayBase, 0));\n grad.addColorStop(0.16, rgba(disp.rayBase, 0.22 * aBase));\n grad.addColorStop(0.4, rgba(disp.rayBase, 0.85 * aBase));\n grad.addColorStop(0.75, rgba(disp.rayBase, 0.36 * aBase));\n grad.addColorStop(1, rgba(disp.rayTip, 0));\n ctx.strokeStyle = grad;\n\n // Soft wide pass → subtle blur/glow.\n ctx.lineWidth = ray.width * (dark ? 3.2 : 2.6);\n ctx.globalAlpha = dark ? 0.22 : 0.18;\n ctx.beginPath();\n ctx.moveTo(originX, originY);\n ctx.lineTo(ex, ey);\n ctx.stroke();\n\n // Crisp core pass.\n ctx.lineWidth = ray.width;\n ctx.globalAlpha = 1;\n ctx.beginPath();\n ctx.moveTo(originX, originY);\n ctx.lineTo(ex, ey);\n ctx.stroke();\n\n // A single glowing dot sits at the very tip — it rides the growing\n // tip but never travels along the fiber.\n if (ray.hasDot) {\n const dtw = reduced ? 1 : 0.5 + 0.5 * Math.sin(now * 2 + ray.dotPhase);\n // Dim dots whose tip still sits inside the convergence zone.\n const tipFade = clamp(effLen / (maxRadius * 0.28), 0, 1);\n ctx.fillStyle = rgba(\n disp.dotTip,\n clamp(\n env * introA * tipFade * (dark ? 0.95 : 1) * (0.45 + 0.55 * dtw) *\n (1 + 0.6 * react),\n 0,\n 1,\n ),\n );\n ctx.beginPath();\n ctx.arc(ex, ey, ray.dotR * (1 + 0.4 * react), 0, Math.PI * 2);\n ctx.fill();\n }\n }\n\n ctx.globalAlpha = 1;\n ctx.globalCompositeOperation = \"source-over\";\n };\n renderRef.current = render;\n\n resize();\n const ro = new ResizeObserver(resize);\n ro.observe(canvas);\n\n if (reduced) {\n render(0);\n return () => ro.disconnect();\n }\n\n const onMove = (e: PointerEvent) => {\n const rect = canvas.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n if (x < 0 || y < 0 || x > rect.width || y > rect.height) {\n pointer.active = false;\n return;\n }\n pointer.x = x;\n pointer.y = y;\n pointer.active = true;\n };\n const onLeave = () => {\n pointer.active = false;\n };\n if (interactive) {\n window.addEventListener(\"pointermove\", onMove, { passive: true });\n window.addEventListener(\"blur\", onLeave);\n }\n\n let last = performance.now();\n const loop = (t: number) => {\n const dt = Math.min(0.05, (t - last) / 1000);\n last = t;\n render(dt);\n raf = requestAnimationFrame(loop);\n };\n raf = requestAnimationFrame(loop);\n\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n if (interactive) {\n window.removeEventListener(\"pointermove\", onMove);\n window.removeEventListener(\"blur\", onLeave);\n }\n };\n }, [density, intro, reduced, interactive]);\n\n return (\n <canvas\n ref={canvasRef}\n aria-hidden\n className={cn(\"absolute inset-0 h-full w-full\", className)}\n />\n );\n}\n\n/* ------------------------------------------------------------------ *\n * ThemeSwitcher — the corner dropdown.\n * ------------------------------------------------------------------ */\n\nfunction ThemeSwitcher({\n theme,\n onChange,\n mode,\n}: {\n theme: RadialBurstThemeId;\n onChange: (id: RadialBurstThemeId) => void;\n mode: \"light\" | \"dark\";\n}) {\n const [open, setOpen] = useState(false);\n const rootRef = useRef<HTMLDivElement>(null);\n const Current = THEME_BY_ID[theme].Icon;\n const dark = mode === \"dark\";\n\n useEffect(() => {\n if (!open) return;\n const onPointer = (e: PointerEvent) => {\n if (!rootRef.current?.contains(e.target as Node)) setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(\"pointerdown\", onPointer);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(\"pointerdown\", onPointer);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, [open]);\n\n return (\n <div ref={rootRef} className=\"relative\">\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-label={`Theme: ${THEME_BY_ID[theme].label}. Change theme`}\n className={cn(\n \"inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-lg border backdrop-blur transition-colors\",\n dark\n ? \"border-white/15 bg-white/5 text-white/80 hover:bg-white/10\"\n : \"border-slate-900/10 bg-white/70 text-slate-600 hover:bg-white\",\n )}\n >\n <Current className=\"h-4 w-4\" />\n </button>\n\n <AnimatePresence>\n {open && (\n <motion.ul\n role=\"listbox\"\n aria-label=\"Theme\"\n initial={{ opacity: 0, y: -6, scale: 0.96 }}\n animate={{ opacity: 1, y: 0, scale: 1 }}\n exit={{ opacity: 0, y: -6, scale: 0.96 }}\n transition={{ type: \"spring\", stiffness: 320, damping: 26 }}\n style={{ transformOrigin: \"top right\" }}\n className={cn(\n \"absolute right-0 top-11 z-30 w-40 overflow-hidden rounded-xl border p-1 shadow-xl backdrop-blur-md\",\n dark\n ? \"border-white/10 bg-[#1b1842]/90 text-white/80\"\n : \"border-slate-900/10 bg-white/90 text-slate-700\",\n )}\n >\n {RADIAL_BURST_THEMES.map((t) => {\n const active = t.id === theme;\n return (\n <li key={t.id}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n onClick={() => {\n onChange(t.id);\n setOpen(false);\n }}\n className={cn(\n \"flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-sm transition-colors\",\n active\n ? dark\n ? \"bg-white/10 text-white\"\n : \"bg-slate-900/5 text-slate-900\"\n : dark\n ? \"hover:bg-white/5\"\n : \"hover:bg-slate-900/5\",\n )}\n >\n <t.Icon className=\"h-4 w-4 shrink-0 opacity-80\" />\n {t.label}\n </button>\n </li>\n );\n })}\n </motion.ul>\n )}\n </AnimatePresence>\n </div>\n );\n}\n\n/* ------------------------------------------------------------------ *\n * RadialBurstHero — full composition: headline over the interactive burst.\n * ------------------------------------------------------------------ */\n\nexport type RadialBurstHeroProps = Omit<\n ComponentProps<\"section\">,\n \"children\" | \"title\"\n> & {\n /** Initial theme; also synced if it changes (e.g. from a site toggle). */\n defaultTheme?: RadialBurstThemeId;\n title?: ReactNode;\n burstProps?: Omit<RadialBurstProps, \"theme\">;\n};\n\nexport function RadialBurstHero({\n className,\n defaultTheme = \"night\",\n title = (\n <>\n The backbone\n <br />\n of global commerce\n </>\n ),\n burstProps,\n ...rest\n}: RadialBurstHeroProps) {\n const [theme, setTheme] = useState<RadialBurstThemeId>(defaultTheme);\n const reduced = useReducedMotion();\n\n // Keep in sync if the consumer (or docs theme toggle) changes defaultTheme.\n useEffect(() => {\n setTheme(defaultTheme);\n }, [defaultTheme]);\n\n const palette = THEME_BY_ID[theme].palette;\n const dark = palette.mode === \"dark\";\n\n return (\n <section\n {...rest}\n data-theme={theme}\n className={cn(\n \"relative isolate flex min-h-[100svh] w-full flex-col overflow-hidden\",\n dark ? \"text-white\" : \"text-slate-900\",\n className,\n )}\n >\n {/* Background gradient — crossfades between themes. */}\n <AnimatePresence initial={false}>\n <motion.div\n key={theme}\n aria-hidden\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.6, ease: \"easeInOut\" }}\n className=\"absolute inset-0 -z-10\"\n style={{ background: palette.bg }}\n />\n </AnimatePresence>\n\n {/* Interactive burst — masked so it fades out below the headline. */}\n <div\n aria-hidden\n className=\"absolute inset-0\"\n style={{\n WebkitMaskImage:\n \"linear-gradient(to bottom, transparent 0%, transparent 24%, #000 48%, #000 100%)\",\n maskImage:\n \"linear-gradient(to bottom, transparent 0%, transparent 24%, #000 48%, #000 100%)\",\n }}\n >\n <RadialBurst theme={theme} {...burstProps} />\n </div>\n\n {/* Theme switcher. */}\n <div className=\"absolute right-5 top-5 z-20 sm:right-8 sm:top-8\">\n <ThemeSwitcher theme={theme} onChange={setTheme} mode={palette.mode} />\n </div>\n\n {/* Headline. */}\n <div className=\"relative z-10 mx-auto flex w-full max-w-5xl flex-col px-6 pt-[14vh]\">\n <motion.h1\n initial={reduced ? false : { opacity: 0, y: 18 }}\n animate={reduced ? undefined : { opacity: 1, y: 0 }}\n transition={{ type: \"spring\", stiffness: 140, damping: 20, delay: 0.1 }}\n className=\"text-center text-balance text-4xl font-semibold leading-[1.05] tracking-tight sm:text-5xl md:text-6xl\"\n >\n {title}\n </motion.h1>\n </div>\n </section>\n );\n}\n\nexport default RadialBurstHero;\n",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Restore the stat row promised by this block.

RadialBurstHero now renders only the headline, and Line 4143 explicitly records that the count-up stat row was removed. The PR objective for hero-radial-burst still calls for a count-up stats section, so this entry now ships the wrong user-facing component.

Also applies to: 4131-4145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@registry.json` at line 4116, RadialBurstHero is missing the count-up stat row
promised in the PR (the hero now only renders the headline); restore the removed
stats block by reintroducing the stat row JSX into the RadialBurstHero return
(between the headline motion.h1 and the section close), using the same
structure/semantics as the original design (a container with numeric counters
and labels), wire it to any existing props or local constants if needed, and
ensure styles/aria match surrounding elements; look for the RadialBurstHero
function and the place where title is rendered (motion.h1) to locate where to
insert the stat row and update any exports/props (RadialBurstHeroProps /
burstProps) if the stats require external data.

@pras75299 pras75299 merged commit 770d920 into main Jun 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant