{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "accordion-gallery",
  "title": "Accordion gallery",
  "description": "A rack of modules where one opens to a full card and the rest close to labelled strips; only transforms move, keyboard and touch included.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/accordion-gallery/accordion-gallery.tsx",
      "content": "import { useEffect, useId, useRef, useState, type CSSProperties, type HTMLAttributes, type KeyboardEvent, type ReactNode } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type AccordionGalleryItem = {\n  /** A stable key. Falls back to the position. */\n  id?: string\n  /** The module's name, on its narrow strip and in its caption. */\n  title: ReactNode\n  /** A short line in the caption card. */\n  description?: ReactNode\n  /** A small mono line above the title in the caption card. */\n  eyebrow?: ReactNode\n  /** What fills the open module: an img or an SVG fills it edge to edge. */\n  content: ReactNode\n}\n\nexport type AccordionGalleryProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {\n  /** The modules, in order. */\n  items: AccordionGalleryItem[]\n  /** Controlled: the open module. */\n  index?: number\n  /** The module open at first, when uncontrolled. */\n  defaultIndex?: number\n  /** Called when a module opens. */\n  onIndexChange?: (index: number) => void\n  /** Open a module when a mouse rests on its strip. Touch and pen open on tap. */\n  openOnHover?: boolean\n  /** The heading level of each strip, so it fits the page's outline. */\n  headingLevel?: 2 | 3 | 4 | 5 | 6\n  /** `auto` stacks the modules vertically when the gallery is narrower than 520 px. */\n  orientation?: 'auto' | 'horizontal' | 'vertical'\n}\n\nconst S = 56 // a closed module's strip\n\n/**\n * A rack of modules on one axis. One is open as a full card; the others close to 56 px strips with a vertical label.\n * Every module has the open module's size, and opening one only slides the modules after it along, so only transform\n * and opacity animate. Hover, tap, focus or the arrow keys open a module (an accordion with one panel always open).\n * Under reduced motion the modules and the caption switch without travel.\n */\nexport function AccordionGallery({\n  items,\n  index,\n  defaultIndex = 0,\n  onIndexChange,\n  openOnHover = true,\n  headingLevel = 3,\n  orientation = 'auto',\n  className,\n  style,\n  ...rest\n}: AccordionGalleryProps) {\n  const n = items.length\n  const [own, setOwn] = useState(defaultIndex)\n  const cur = Math.min(Math.max(index ?? own, 0), Math.max(n - 1, 0))\n  const id = useId()\n  const tabs = useRef<(HTMLButtonElement | null)[]>([])\n  const hover = useRef(0)\n\n  useEffect(() => () => clearTimeout(hover.current), [])\n\n  const select = (k: number) => {\n    clearTimeout(hover.current)\n    if (k === cur) return\n    setOwn(k)\n    onIndexChange?.(k)\n  }\n\n  const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>, k: number) => {\n    const to = { ArrowRight: k + 1, ArrowDown: k + 1, ArrowLeft: k - 1, ArrowUp: k - 1, Home: 0, End: n - 1 }[e.key]\n    if (to === undefined) return\n    e.preventDefault()\n    // Moving focus opens the module, through its focus handler.\n    tabs.current[(to + n) % n]?.focus()\n  }\n\n  return (\n    <div\n      data-orientation={orientation}\n      style={{ '--ag-n': n, ...style } as CSSProperties}\n      className={cn('group/ag w-full', orientation === 'auto' && '@container/ag', className)}\n      {...rest}\n    >\n      <div\n        className={cn(\n          'relative h-[372px] w-full overflow-hidden rounded-[calc(var(--radius)*2+2px)] bg-card shadow-[0_0_0_1px_color-mix(in_oklab,var(--foreground)_9%,transparent),0_1px_2px_rgba(0,0,0,0.03)]',\n          '@max-[520px]/ag:h-[calc(300px+(var(--ag-n)-1)*56px)] group-data-[orientation=vertical]/ag:h-[calc(300px+(var(--ag-n)-1)*56px)]',\n        )}\n      >\n        {items.map((item, k) => {\n          const on = k === cur\n          const tab = `${id}-t${k}`\n          const panel = `${id}-p${k}`\n          // Modules up to the open one stack from the start; the rest sit one open module further on.\n          const after = k > cur\n          const offset = `calc(${after ? '100% + ' : ''}${(after ? k - 1 : k) * S}px)`\n          return (\n            <div\n              key={item.id ?? k}\n              data-on={on ? '' : undefined}\n              style={{ zIndex: k + 1, '--ag-p': offset } as CSSProperties}\n              className={cn(\n                'group/m absolute top-0 left-0 flex h-full w-[calc(100%-(var(--ag-n)-1)*56px)] bg-card',\n                '[transform:translateX(var(--ag-p))] transition-transform duration-280 ease-out-quint will-change-transform motion-reduce:transition-none',\n                k > 0 &&\n                  'shadow-[-1px_0_0_color-mix(in_oklab,var(--foreground)_9%,transparent),-14px_0_22px_-20px_rgb(0_0_0/0.28)] dark:shadow-[-1px_0_0_color-mix(in_oklab,var(--foreground)_9%,transparent),-14px_0_22px_-20px_rgb(0_0_0/0.7)]',\n                '@max-[520px]/ag:h-[300px] @max-[520px]/ag:w-full @max-[520px]/ag:flex-col @max-[520px]/ag:[transform:translateY(var(--ag-p))]',\n                'group-data-[orientation=vertical]/ag:h-[300px] group-data-[orientation=vertical]/ag:w-full group-data-[orientation=vertical]/ag:flex-col group-data-[orientation=vertical]/ag:[transform:translateY(var(--ag-p))]',\n                k > 0 &&\n                  '@max-[520px]/ag:shadow-[0_-1px_0_var(--border),0_-14px_22px_-20px_rgb(0_0_0/0.28)] @max-[520px]/ag:dark:shadow-[0_-1px_0_var(--border),0_-14px_22px_-20px_rgb(0_0_0/0.7)]',\n                k > 0 &&\n                  'group-data-[orientation=vertical]/ag:shadow-[0_-1px_0_var(--border),0_-14px_22px_-20px_rgb(0_0_0/0.28)] group-data-[orientation=vertical]/ag:dark:shadow-[0_-1px_0_var(--border),0_-14px_22px_-20px_rgb(0_0_0/0.7)]',\n              )}\n            >\n              <div\n                role=\"heading\"\n                aria-level={headingLevel}\n                className=\"h-full w-14 shrink-0 @max-[520px]/ag:h-14 @max-[520px]/ag:w-full group-data-[orientation=vertical]/ag:h-14 group-data-[orientation=vertical]/ag:w-full\"\n              >\n                <button\n                  ref={(el) => {\n                    tabs.current[k] = el\n                  }}\n                  type=\"button\"\n                  id={tab}\n                  aria-expanded={on}\n                  aria-controls={panel}\n                  // The open module cannot close: one is always open.\n                  aria-disabled={on || undefined}\n                  onClick={() => select(k)}\n                  onFocus={() => select(k)}\n                  onKeyDown={(e) => onKeyDown(e, k)}\n                  onPointerEnter={(e) => {\n                    if (!openOnHover || e.pointerType !== 'mouse') return\n                    clearTimeout(hover.current)\n                    hover.current = window.setTimeout(() => select(k), 70)\n                  }}\n                  onPointerLeave={() => clearTimeout(hover.current)}\n                  className={cn(\n                    'group/t flex size-full cursor-pointer flex-col items-center justify-between bg-card py-3.5 text-muted-foreground [-webkit-tap-highlight-color:transparent] aria-disabled:cursor-default',\n                    'focus-visible:rounded-[calc(var(--radius)+2px)] focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-ring',\n                    '@max-[520px]/ag:flex-row @max-[520px]/ag:px-3.5 @max-[520px]/ag:py-0 group-data-[orientation=vertical]/ag:flex-row group-data-[orientation=vertical]/ag:px-3.5 group-data-[orientation=vertical]/ag:py-0',\n                  )}\n                >\n                  <span\n                    aria-hidden\n                    className=\"grid h-7 min-w-7 shrink-0 place-items-center rounded-full bg-muted px-1.5 font-mono text-[11px] leading-none font-medium text-muted-foreground tabular-nums group-hover/t:text-foreground group-data-on/m:bg-foreground group-data-on/m:text-card\"\n                  >\n                    {String(k + 1).padStart(2, '0')}\n                  </span>\n                  <span className=\"flex flex-1 flex-col items-center gap-3.5 pt-3 pb-3 @max-[520px]/ag:flex-row @max-[520px]/ag:gap-3 @max-[520px]/ag:py-0 @max-[520px]/ag:pl-3.5 group-data-[orientation=vertical]/ag:flex-row group-data-[orientation=vertical]/ag:gap-3 group-data-[orientation=vertical]/ag:py-0 group-data-[orientation=vertical]/ag:pl-3.5\">\n                    <span\n                      className={cn(\n                        'mt-auto mb-auto rotate-180 text-[15px] leading-none font-medium tracking-[-0.01em] whitespace-nowrap [writing-mode:vertical-rl] group-hover/t:text-foreground group-data-on/m:text-foreground',\n                        '@max-[520px]/ag:mt-0 @max-[520px]/ag:mb-0 @max-[520px]/ag:rotate-0 @max-[520px]/ag:[writing-mode:horizontal-tb] group-data-[orientation=vertical]/ag:mt-0 group-data-[orientation=vertical]/ag:mb-0 group-data-[orientation=vertical]/ag:rotate-0 group-data-[orientation=vertical]/ag:[writing-mode:horizontal-tb]',\n                      )}\n                    >\n                      {item.title}\n                    </span>\n                  </span>\n                  <span\n                    aria-hidden\n                    className=\"relative size-2 shrink-0 rounded-full bg-muted @max-[520px]/ag:mr-3.5 @max-[520px]/ag:ml-auto group-data-[orientation=vertical]/ag:mr-3.5 group-data-[orientation=vertical]/ag:ml-auto\"\n                  >\n                    <span className=\"absolute inset-0 rounded-full bg-primary opacity-0 transition-opacity duration-120 ease-out-quint group-data-on/m:opacity-100 motion-reduce:transition-none\" />\n                  </span>\n                </button>\n              </div>\n              <div\n                id={panel}\n                role=\"region\"\n                aria-labelledby={tab}\n                inert={!on}\n                className=\"relative my-1.5 mr-1.5 min-h-0 min-w-0 flex-1 overflow-hidden rounded-[calc(var(--radius)+2px)] bg-muted @max-[520px]/ag:mt-0 @max-[520px]/ag:ml-1.5 group-data-[orientation=vertical]/ag:mt-0 group-data-[orientation=vertical]/ag:ml-1.5\"\n              >\n                <div className=\"absolute inset-0 [&>img]:size-full [&>img]:object-cover [&>svg]:size-full\">{item.content}</div>\n                {(item.description || item.eyebrow) && (\n                  <div\n                    className={cn(\n                      'absolute right-3 bottom-3 left-3 grid max-w-[300px] translate-y-1.5 gap-1 rounded-[calc(var(--radius)+2px)] bg-card px-4 py-3.5 opacity-0 shadow-[0_1px_2px_rgba(0,0,0,0.14),0_10px_24px_-10px_rgba(0,0,0,0.4)]',\n                      'transition-[opacity,translate] ease-out-quint [transition-duration:180ms,220ms] motion-reduce:transition-none',\n                      'group-data-on/m:translate-y-0 group-data-on/m:opacity-100 group-data-on/m:delay-90',\n                    )}\n                  >\n                    {item.eyebrow && (\n                      <span className=\"font-mono text-[11px] leading-none font-medium tracking-[0.04em] text-muted-foreground tabular-nums\">{item.eyebrow}</span>\n                    )}\n                    <b className=\"text-[15px] leading-[1.3] font-semibold tracking-[-0.01em]\">{item.title}</b>\n                    {item.description && <span className=\"text-[12.5px] leading-[1.45] text-muted-foreground\">{item.description}</span>}\n                  </div>\n                )}\n              </div>\n            </div>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "surfaces"
  ],
  "type": "registry:component"
}