{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "feature-highlights", "type": "registry:ui", "title": "Feature Highlights", "description": "Progressive reveal feature list with sliding accent indicator, grid-template-rows expand, and staggered entrance.", "dependencies": [], "files": [ { "path": "registry/ruixenui/feature-highlights.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ═══════════════════════════════════════════════════════════\n Feature Highlights — Staged Spotlight.\n\n A split-layout feature showcase with an auto-advancing\n image stage and a rack-focus crossfade between visuals.\n\n Layout:\n Mobile: image (top) → feature list (bottom)\n Desktop: feature list (left) ↔ image stage (right)\n\n Fundamentally different from every accordion in ruixen:\n • faq-auto-accordion: spring traveling accent, motion physics\n • faq-scroll-accordion: self-contained scroll cascade\n • feature-highlights: IMAGE STAGE + auto-play + rack-focus\n\n Rack-focus crossfade:\n Active: opacity 1, scale 1, blur 0\n Inactive: opacity 0, scale 0.94, blur 6px\n The blur+scale creates a camera depth-of-field effect —\n incoming images \"focus in\" from a slightly larger, blurred\n state, outgoing images \"defocus\" and recede.\n\n Auto-play:\n A progress bar fills from left to right over N seconds.\n On completion, auto-advances to the next feature (loops).\n The progress bar uses CSS @keyframes scaleX(0→1) with\n transform-origin: left. The key prop resets the animation\n on active change. Pauses on hover via animation-play-state.\n\n Feature list:\n Sliding accent indicator via ResizeObserver — tracks the\n active item's position and height in real time.\n Description uses grid-template-rows 0fr→1fr for smooth\n height animation. Description text has 80ms delayed\n fade+slide after the container opens.\n\n Zero dependencies. No embla, no framer-motion, no next/image.\n ═══════════════════════════════════════════════════════════ */\n\nexport interface FeatureItem {\n title: string;\n description: string;\n image?: React.ReactNode;\n}\n\nexport interface FeatureHighlightsProps {\n title?: string;\n features?: FeatureItem[];\n /** Seconds per feature during auto-play. 0 to disable. */\n autoPlayInterval?: number;\n /** Index of the initially active feature. */\n defaultActive?: number;\n className?: string;\n}\n\nconst defaultFeatures: FeatureItem[] = [\n {\n title: \"AI-Powered Automation\",\n description:\n \"Streamline workflows with intelligent automation that learns from your patterns and adapts to your needs.\",\n },\n {\n title: \"Real-Time Analytics\",\n description:\n \"Monitor metrics live with sub-second latency. Dashboards update instantly as data flows in.\",\n },\n {\n title: \"Seamless Integrations\",\n description:\n \"Connect with the tools you already use. One-click setup, zero configuration drift.\",\n },\n {\n title: \"Scalable Infrastructure\",\n description:\n \"Grow without limits. Auto-scaling handles traffic spikes so you never think about capacity.\",\n },\n];\n\nexport function FeatureHighlights({\n title = \"Why we're different\",\n features = defaultFeatures,\n autoPlayInterval = 5,\n defaultActive = 0,\n className,\n}: FeatureHighlightsProps) {\n const [active, setActive] = React.useState(defaultActive);\n const [paused, setPaused] = React.useState(false);\n const itemRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n const listRef = React.useRef(null);\n const [indicator, setIndicator] = React.useState({ top: 0, height: 0 });\n\n // ── Accent indicator tracking ──────────────────────────\n // ResizeObserver catches height changes as the description\n // expands — the indicator stretches in real time.\n React.useEffect(() => {\n const el = itemRefs.current[active];\n const list = listRef.current;\n if (!el || !list) return;\n\n const update = () => {\n const listRect = list.getBoundingClientRect();\n const elRect = el.getBoundingClientRect();\n setIndicator({\n top: elRect.top - listRect.top,\n height: elRect.height,\n });\n };\n\n update();\n\n const ro = new ResizeObserver(update);\n ro.observe(el);\n return () => ro.disconnect();\n }, [active]);\n\n // ── Auto-advance callback ──────────────────────────────\n const advance = React.useCallback(() => {\n setActive((prev) => (prev + 1) % features.length);\n }, [features.length]);\n\n return (\n setPaused(true)}\n onMouseLeave={() => setPaused(false)}\n >\n
\n {title && (\n

\n {title}\n

\n )}\n\n
\n {/* ── Feature list ──────────────────────────── */}\n \n {/* Track line */}\n
\n\n {/* Sliding accent indicator */}\n \n\n {features.map((feature, i) => {\n const isActive = i === active;\n\n return (\n {\n itemRefs.current[i] = el;\n }}\n type=\"button\"\n className=\"relative flex w-full cursor-pointer flex-col items-start py-3 pl-5 text-left\"\n onClick={() => setActive(i)}\n >\n {/* Index + Title */}\n
\n \n {String(i + 1).padStart(2, \"0\")}\n \n \n {feature.title}\n \n
\n\n {/* Expandable description */}\n \n
\n \n {feature.description}\n

\n
\n
\n\n {/* Progress bar — fills during auto-play */}\n {isActive && autoPlayInterval > 0 && (\n
\n \n
\n )}\n \n );\n })}\n
\n\n {/* ── Image stage — rack-focus crossfade ─────── */}\n
\n {features.map((feature, i) => (\n \n {feature.image || (\n
\n \n {feature.title}\n \n
\n )}\n
\n ))}\n
\n \n \n\n \n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/feature-highlights.tsx" } ] }