{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "split-layout-accordion", "type": "registry:block", "title": "Split Layout Accordion", "description": "SplitLayoutGlass Accordion Components", "dependencies": [ "lucide-react", "react" ], "registryDependencies": [ "cn" ], "files": [ { "path": "components/glass/composite/split-layout-accordion.tsx", "type": "registry:component", "content": "/* eslint-disable react-refresh/only-export-components */\n/**\n * SplitLayoutGlass Accordion Components\n *\n * Mobile-specific components that render content in accordion pattern.\n * Details expand below the selected item instead of showing in a separate panel.\n *\n * @module split-layout-accordion\n */\n\nimport { forwardRef, type ReactNode, useId, useRef, useEffect, useCallback, useState } from 'react';\nimport { ChevronDown } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { GlassCard } from '@/components/glass/ui/glass-card';\nimport { useSplitLayout, useSplitLayoutOptional } from './split-layout-context';\n\n// ========================================\n// ACCORDION ROOT\n// ========================================\n\n/**\n * Props for SplitLayoutAccordion.Root component\n * Container for accordion items in mobile view\n */\nexport interface SplitLayoutAccordionRootProps extends React.HTMLAttributes {\n children: ReactNode;\n /**\n * ARIA label for the accordion region\n * @default \"Content accordion\"\n */\n label?: string;\n}\n\nconst SplitLayoutAccordionRoot = forwardRef(\n ({ children, label = 'Content accordion', className, ...props }, ref) => {\n const context = useSplitLayoutOptional();\n const intensity = context?.intensity ?? 'medium';\n\n return (\n \n
\n {children}\n
\n \n );\n }\n);\n\nSplitLayoutAccordionRoot.displayName = 'SplitLayoutAccordion.Root';\n\n// ========================================\n// ACCORDION ITEM\n// ========================================\n\n/**\n * Props for SplitLayoutAccordion.Item component\n * A single accordion item with trigger and collapsible content\n */\nexport interface SplitLayoutAccordionItemProps extends Omit<\n React.HTMLAttributes,\n 'children'\n> {\n /**\n * Unique key for this item (used for selection)\n */\n itemKey: string;\n /**\n * Content shown in the trigger/header area\n */\n trigger: ReactNode;\n /**\n * Content shown when expanded (details)\n */\n children: ReactNode;\n /**\n * Disable this item\n * @default false\n */\n disabled?: boolean;\n}\n\nconst SplitLayoutAccordionItem = forwardRef(\n ({ itemKey, trigger, children, disabled = false, className, ...props }, ref) => {\n const { selectedKey, setSelectedKey } = useSplitLayout();\n const isExpanded = selectedKey === itemKey;\n const contentRef = useRef(null);\n const [contentHeight, setContentHeight] = useState(null);\n const triggerId = useId();\n const contentId = useId();\n\n // Measure content height for smooth animation\n useEffect(() => {\n if (contentRef.current) {\n setContentHeight(contentRef.current.scrollHeight);\n }\n }, [children, isExpanded]);\n\n const handleToggle = useCallback(() => {\n if (disabled) return;\n setSelectedKey(isExpanded ? null : itemKey);\n }, [disabled, isExpanded, itemKey, setSelectedKey]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n\n switch (e.key) {\n case 'Enter':\n case ' ':\n e.preventDefault();\n handleToggle();\n break;\n case 'ArrowDown': {\n e.preventDefault();\n // Focus next item trigger\n const next = (e.currentTarget as HTMLElement).parentElement?.nextElementSibling;\n const nextTrigger = next?.querySelector('[data-accordion-trigger]') as HTMLElement;\n nextTrigger?.focus();\n break;\n }\n case 'ArrowUp': {\n e.preventDefault();\n // Focus previous item trigger\n const prev = (e.currentTarget as HTMLElement).parentElement?.previousElementSibling;\n const prevTrigger = prev?.querySelector('[data-accordion-trigger]') as HTMLElement;\n prevTrigger?.focus();\n break;\n }\n case 'Home': {\n e.preventDefault();\n // Focus first item trigger\n const first = (e.currentTarget as HTMLElement)\n .closest('[data-split-accordion]')\n ?.querySelector('[data-accordion-trigger]') as HTMLElement;\n first?.focus();\n break;\n }\n case 'End': {\n e.preventDefault();\n // Focus last item trigger\n const triggers = (e.currentTarget as HTMLElement)\n .closest('[data-split-accordion]')\n ?.querySelectorAll('[data-accordion-trigger]');\n const last = triggers?.[triggers.length - 1] as HTMLElement;\n last?.focus();\n break;\n }\n }\n },\n [disabled, handleToggle]\n );\n\n return (\n \n {/* Trigger/Header */}\n \n
{trigger}
\n \n \n\n {/* Content */}\n \n
{children}
\n \n \n );\n }\n);\n\nSplitLayoutAccordionItem.displayName = 'SplitLayoutAccordion.Item';\n\n// ========================================\n// COMPOUND EXPORT\n// ========================================\n\n/**\n * SplitLayoutAccordion compound component for mobile accordion view\n *\n * @example\n * ```tsx\n * // Use with mobileMode=\"accordion\" in Provider\n * \n * {isMobile ? (\n * \n * {years.map((year) => (\n * }\n * >\n * \n * \n * ))}\n * \n * ) : (\n * ...\n * )}\n * \n * ```\n */\nexport const SplitLayoutAccordion = {\n Root: SplitLayoutAccordionRoot,\n Item: SplitLayoutAccordionItem,\n};\n" } ], "categories": [ "composite" ] }