000h-cojeev
uiaccordion-gallerysource: ok · hit
Accordion Gallery
A selected image panel opens to reveal its label, story and action.
npx shadcn add "https://000h.cojeev.com/r/accordion-gallery.json"free
Where it goes
gallery
Interaction
scroll-revealaccordiondropdown
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightRegistry dependencies
https://cojeev.com/ui/r/cojeev.json
Files2 · source cached 10/8/2026, 3:57:29 PM
registry/cojeev/ui/accordion-gallery.tsx
"use client";
/* eslint-disable @next/next/no-img-element -- Installable React image gallery. */
import * as React from "react";
import { cn } from "@/lib/utils";
import { useMotionVisibility } from "@/lib/cojeev-motion/use-motion-visibility";
import { useGalleryRef, useGallerySelection } from "@/lib/cojeev/reference-gallery-motion";
export type AccordionGalleryItem={id:string;src:string;alt:string;title:string;description?:string;href?:string};
export type AccordionGalleryProps=Omit<React.ComponentProps<"div">,"onChange"> & {items:readonly AccordionGalleryItem[];defaultIndex?:number;selectedIndex?:number;onSelectionChange?:(index:number)=>void;orientation?:"horizontal"|"vertical"};
export function AccordionGallery({items,defaultIndex=0,selectedIndex,onSelectionChange,orientation="horizontal",className,ref,...props}:AccordionGalleryProps){
const host=React.useRef<HTMLDivElement>(null),buttons=React.useRef<(HTMLButtonElement|null)[]>([]),id=React.useId();
const {enabled,inView}=useMotionVisibility(host);
const {selected,select}=useGallerySelection(items.length,defaultIndex,selectedIndex,onSelectionChange);
const key=(event:React.KeyboardEvent<HTMLButtonElement>,index:number)=>{let next=index;if(event.key==="ArrowRight"||event.key==="ArrowDown")next=(index+1)%items.length;else if(event.key==="ArrowLeft"||event.key==="ArrowUp")next=(index-1+items.length)%items.length;else if(event.key==="Home")next=0;else if(event.key==="End")next=items.length-1;else return;event.preventDefault();select(next);buttons.current[next]?.focus();};
return <div {...props} ref={useGalleryRef(host,ref)} data-slot="accordion-gallery" data-motion={enabled&&inView ? "on":"off"} data-orientation={orientation} className={cn("v-accordion-gallery",className)}>
{items.length===0 ? <p>No gallery items.</p> : items.map((item,index)=><section key={item.id} className="v-accordion-gallery__panel" data-selected={selected===index}>
<img className="v-accordion-gallery__image" src={item.src} alt={item.alt}/>
<h3 className="v-accordion-gallery__heading"><button type="button" ref={node=>{buttons.current[index]=node;}} aria-expanded={selected===index} aria-controls={`${id}-${index}`} onClick={()=>select(index)} onKeyDown={event=>key(event,index)}><span className="v-accordion-gallery__number">{String(index+1).padStart(2,"0")}</span><span>{item.title}</span></button></h3>
<div id={`${id}-${index}`} className="v-accordion-gallery__story" hidden={selected!==index}>{item.description&&<p>{item.description}</p>}{item.href&&<a href={item.href}>Explore {item.title}<span aria-hidden="true"> ↗</span></a>}</div>
</section>)}
</div>;
}
registry/cojeev/styles/accordion-gallery.css
@layer cojeev-states {
.v-accordion-gallery{display:flex;gap:var(--s-3);width:100%;min-width:0;min-height:380px;isolation:isolate}.v-accordion-gallery__panel{position:relative;overflow:hidden;flex:1 1 0;min-width:64px;border:1px solid var(--v-border);border-radius:var(--r-card);background:var(--v-paper);transition:flex-grow .55s cubic-bezier(.22,1,.36,1)}.v-accordion-gallery__panel[data-selected=true]{flex-grow:4}.v-accordion-gallery__image{position:absolute;width:100%;height:100%;object-fit:cover;inset:0;filter:grayscale(.7);opacity:.55;transition:filter .5s,opacity .5s,transform .5s}.v-accordion-gallery__panel[data-selected=true] .v-accordion-gallery__image{filter:none;opacity:1;transform:scale(1.025)}.v-accordion-gallery__heading{position:relative;z-index:1;margin:0}.v-accordion-gallery__heading button{appearance:none;width:100%;border:0;border-bottom:1px solid var(--v-border);background:var(--v-paper);color:var(--v-ink);padding:var(--s-4) var(--s-3);font:inherit;font-family:var(--font-display);font-size:var(--fs-lead);display:flex;gap:var(--s-3);align-items:center;text-align:left;cursor:pointer;min-height:56px;overflow-wrap:anywhere}.v-accordion-gallery__number{font-size:var(--fs-meta);flex-shrink:0}.v-accordion-gallery__story{position:absolute;inset:auto 12px 12px;background:var(--v-paper);color:var(--v-ink);padding:var(--s-4);border-radius:var(--r-md);max-height:60%;overflow:auto;font-size:var(--fs-control)}.v-accordion-gallery__story p{margin:0 0 var(--s-3);line-height:1.5}.v-accordion-gallery__story a{color:inherit;font-weight:var(--fw-semibold);text-underline-offset:4px}.v-accordion-gallery button:focus-visible,.v-accordion-gallery a:focus-visible{outline:2px solid var(--v-ink);outline-offset:-4px}.v-accordion-gallery[data-motion=off] *{transition:none}.v-accordion-gallery[data-orientation=vertical]{flex-direction:column;height:570px}.v-accordion-gallery[data-orientation=vertical] .v-accordion-gallery__panel{min-height:56px}.v-accordion-gallery[data-orientation=vertical] .v-accordion-gallery__panel[data-selected=true]{flex-grow:6}@media(max-width:600px){.v-accordion-gallery{flex-direction:column;height:570px}.v-accordion-gallery__panel{min-height:56px}.v-accordion-gallery__panel[data-selected=true]{flex-grow:6}.v-accordion-gallery__heading button{padding:var(--s-3) var(--s-4)}}@media(prefers-reduced-motion:reduce){.v-accordion-gallery *{transition:none}}
}