Blocks
Ready-made layouts built from the organ viewer and the anatomy tree. Each block installs its page and components as source, so you can swap the organ and change the layout.
Blocks install the OrganUI components they use as dependencies. Pages install in Next.js; other frameworks get the components. Like the organ viewer, blocks need one of shadcn's Base UI styles.
Explorer 01
An organ viewer with an anatomy panel that docks beside it on wide screens and opens as a sheet on phones. Heart model licensed CC BY 4.0.
bunx shadcn@4.22.1 add @organui/explorer-01
Files
app/explorer/page.tsximport { OrganExplorer } from "@/components/organ-explorer" import { heartModel } from "@/components/heart" export default function Page() { return ( <main className="mx-auto flex min-h-svh w-full max-w-5xl flex-col gap-6 p-6 md:p-10"> <div className="flex flex-col gap-1"> <h1 className="text-2xl font-semibold tracking-tight"> {heartModel.name} </h1> <p className="text-muted-foreground"> Rotate the model, or open the anatomy panel to find and hide structures. </p> </div> <OrganExplorer model={heartModel} /> </main> ) }components/organ-explorer.tsx"use client" import * as React from "react" import { cn } from "cn" import { AnatomyTree } from "@/components/ui/anatomy-tree" import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/components/ui/sheet" import { Toggle } from "@/components/ui/toggle" import { OrganViewer, organAnatomyNodes, organDefaultVisibility, type OrganModel, } from "@/components/organ-viewer" // Matches the lg breakpoint where the panel fits beside the viewer. const WIDE = "(min-width: 64rem)" function subscribeWide(onChange: () => void) { const query = matchMedia(WIDE) query.addEventListener("change", onChange) return () => query.removeEventListener("change", onChange) } // The anatomy tree drives the viewer, and picks in 3D flow back to the tree. // Like the explorers, the panel starts closed so the 3D view stands alone. // Phones open it as a sheet. export function OrganExplorer({ model, className, }: { model: OrganModel className?: string }) { const [data] = React.useState(() => organAnatomyNodes(model)) const [selectedId, setSelectedId] = React.useState<string | null>(null) const [visibility, setVisibility] = React.useState(() => organDefaultVisibility(model) ) const [view, setView] = React.useState<string | null>(model.defaultView) const selected = model.structures.find(({ id }) => id === selectedId) const [open, setOpen] = React.useState(false) const wide = React.useSyncExternalStore( subscribeWide, () => matchMedia(WIDE).matches, () => true ) const panel = React.useId() const panelRef = React.useRef<HTMLDivElement>(null) const sheetRef = React.useRef<HTMLDivElement>(null) const toggle = React.useRef<HTMLButtonElement>(null) React.useEffect(() => { if (open && wide) panelRef.current?.querySelector<HTMLElement>("input")?.focus() }, [open, wide]) const close = () => { setOpen(false) toggle.current?.focus() } const tree = (className: string) => ( <AnatomyTree data={data} selectedId={selectedId} onSelectionChange={setSelectedId} visibility={visibility} onVisibilityChange={setVisibility} label={`${model.name} anatomy`} // Small organs open fully; the skeleton and muscles start collapsed. defaultExpandedIds={ model.structures.length <= 40 ? data.map(({ id }) => id) : [] } className={className} /> ) return ( <div className={cn( "grid gap-4", open && wide && "grid-cols-[minmax(0,1fr)_18rem]", className )} > <div className="flex min-w-0 flex-col gap-3"> <OrganViewer model={model} selectedId={selectedId} onSelectionChange={setSelectedId} visibility={visibility} onViewChange={setView} className="h-[28rem]" /> <div className="flex items-start justify-between gap-4"> <p className="text-sm text-muted-foreground" role="status"> {selected ? `${selected.label}: ${selected.description ?? selected.group}` : `View: ${view ?? "Custom view"}. Select a structure to learn about it.`} </p> <Toggle ref={toggle} variant="outline" pressed={open} onPressedChange={(pressed) => (pressed ? setOpen(true) : close())} aria-controls={open && wide ? panel : undefined} className="shrink-0" > <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" data-icon="inline-start" > <rect width="18" height="18" x="3" y="3" rx="2" /> <path d="M15 3v18" /> </svg> Anatomy </Toggle> </div> </div> {open && wide && ( <div id={panel} ref={panelRef} onKeyDown={(event) => { if (event.key === "Escape") close() }} > {tree("max-h-[28rem] overflow-auto")} </div> )} <Sheet open={open && !wide} onOpenChange={(next) => { if (!next) setOpen(false) }} > <SheetContent ref={sheetRef} side="bottom" initialFocus={() => sheetRef.current?.querySelector("input")} finalFocus={toggle} className="gap-0 data-[side=bottom]:h-[70svh]" > <SheetHeader className="pr-12"> <SheetTitle>{model.name} anatomy</SheetTitle> </SheetHeader> {tree( "min-h-0 flex-1 overflow-auto rounded-none border-x-0 border-b-0" )} </SheetContent> </Sheet> </div> ) }
Explorer 02
A full-height anatomy workspace with the tree in a sidebar and a details card to hide or isolate the selected structure. Heart model licensed CC BY 4.0.
bunx shadcn@4.22.1 add @organui/explorer-02
Files
app/workspace/page.tsximport { AnatomyWorkspace } from "@/components/anatomy-workspace" import { heartModel } from "@/components/heart" export default function Page() { return <AnatomyWorkspace model={heartModel} /> }components/anatomy-workspace.tsx"use client" import * as React from "react" import { cn } from "cn" import { AnatomyTree } from "@/components/ui/anatomy-tree" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/components/ui/sheet" import { OrganViewer, organAnatomyNodes, organDefaultVisibility, type OrganModel, } from "@/components/organ-viewer" // Matches the lg breakpoint where the sidebar is shown. const WIDE = "(min-width: 64rem)" function subscribeWide(onChange: () => void) { const query = matchMedia(WIDE) query.addEventListener("change", onChange) return () => query.removeEventListener("change", onChange) } // A full-height workspace: the anatomy tree stays in a sidebar on wide // screens and opens as a sheet on phones, and a details card acts on the // selected structure. export function AnatomyWorkspace({ model, className, }: { model: OrganModel className?: string }) { const [data] = React.useState(() => organAnatomyNodes(model)) const [selectedId, setSelectedId] = React.useState<string | null>(null) const [visibility, setVisibility] = React.useState<Record<string, boolean>>( () => organDefaultVisibility(model) ) const [open, setOpen] = React.useState(false) const wide = React.useSyncExternalStore( subscribeWide, () => matchMedia(WIDE).matches, () => true ) const sheetRef = React.useRef<HTMLDivElement>(null) const selected = model.structures.find(({ id }) => id === selectedId) const hidden = model.structures.filter( ({ id }) => visibility[id] === false ).length const tree = (className: string) => ( <AnatomyTree data={data} selectedId={selectedId} onSelectionChange={setSelectedId} visibility={visibility} onVisibilityChange={setVisibility} label={`${model.name} anatomy`} defaultExpandedIds={ model.structures.length <= 40 ? data.map(({ id }) => id) : [] } className={className} /> ) return ( <div className={cn("flex h-svh flex-col", className)}> <header className="flex h-14 shrink-0 items-center gap-3 border-b px-4"> {!wide && ( <Button variant="outline" size="icon" aria-label="Open anatomy" onClick={() => setOpen(true)} > <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" > <rect width="18" height="18" x="3" y="3" rx="2" /> <path d="M9 3v18" /> </svg> </Button> )} <h1 className="font-medium">{model.name}</h1> <p className="truncate text-sm text-muted-foreground"> {model.structures.length} structures {hidden > 0 && `, ${hidden} hidden`} </p> <Button variant="ghost" size="sm" className="ml-auto" disabled={hidden === 0} onClick={() => setVisibility({})} > Show all </Button> </header> <div className="flex min-h-0 flex-1"> {wide && ( <aside aria-label="Anatomy" className="flex w-72 shrink-0 flex-col border-r" > {tree("min-h-0 flex-1 overflow-auto rounded-none border-0")} </aside> )} <main className="flex min-w-0 flex-1 flex-col gap-4 p-4"> <OrganViewer model={model} selectedId={selectedId} onSelectionChange={setSelectedId} visibility={visibility} className="h-auto min-h-72 flex-1" /> <Card size="sm" className="shrink-0"> <CardHeader> <CardTitle> {selected?.label ?? "No structure selected"} </CardTitle> <CardDescription> {selected?.group ?? "Select a structure in the model or the anatomy list."} </CardDescription> </CardHeader> {selected && ( <> {selected.description && ( <CardContent> <p>{selected.description}</p> </CardContent> )} <CardFooter className="gap-2"> <Button variant="outline" size="sm" onClick={() => setVisibility((current) => ({ ...current, [selected.id]: current[selected.id] === false, })) } > {visibility[selected.id] === false ? "Show" : "Hide"} </Button> <Button variant="outline" size="sm" onClick={() => setVisibility( Object.fromEntries( model.structures .filter(({ id }) => id !== selected.id) .map(({ id }) => [id, false]) ) ) } > Isolate </Button> <Button variant="ghost" size="sm" className="ml-auto" onClick={() => setSelectedId(null)} > Clear </Button> </CardFooter> </> )} </Card> </main> </div> <Sheet open={open && !wide} onOpenChange={setOpen}> <SheetContent ref={sheetRef} side="left" initialFocus={() => sheetRef.current?.querySelector("input")} className="gap-0" > <SheetHeader className="pr-12"> <SheetTitle>{model.name} anatomy</SheetTitle> </SheetHeader> {tree( "min-h-0 flex-1 overflow-auto rounded-none border-x-0 border-b-0" )} </SheetContent> </Sheet> </div> ) }
Explorer 03
One viewer that switches between several organs from a menu, here the heart, kidneys, stomach and pancreas. Models licensed CC BY 4.0 and CC BY-SA 2.1 JP.
bunx shadcn@4.22.1 add @organui/explorer-03
Files
app/organs/page.tsximport { OrganSwitcher } from "@/components/organ-switcher" import { heartModel } from "@/components/heart" import { kidneysModel } from "@/components/kidneys" import { pancreasModel } from "@/components/pancreas" import { stomachModel } from "@/components/stomach" export default function Page() { return ( <main className="mx-auto flex min-h-svh w-full max-w-5xl flex-col gap-6 p-6 md:p-10"> <div className="flex flex-col gap-1"> <h1 className="text-2xl font-semibold tracking-tight">Organs</h1> <p className="text-muted-foreground"> Pick an organ, then select a structure to learn about it. </p> </div> <OrganSwitcher models={[heartModel, kidneysModel, stomachModel, pancreasModel]} /> </main> ) }components/organ-switcher.tsx"use client" import * as React from "react" import { cn } from "cn" import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { OrganViewer, type OrganModel } from "@/components/organ-viewer" // One viewer for several organs. Switching remounts the viewer, so each organ // opens in its own default view with nothing selected. export function OrganSwitcher({ models, defaultOrgan = models[0]?.name, className, }: { models: readonly OrganModel[] /** Name of the organ shown first. Defaults to the first model. */ defaultOrgan?: string className?: string }) { const [organ, setOrgan] = React.useState(defaultOrgan) const [selectedId, setSelectedId] = React.useState<string | null>(null) const model = models.find(({ name }) => name === organ) ?? models[0] if (!model) return null const selected = model.structures.find(({ id }) => id === selectedId) return ( <div className={cn("flex flex-col gap-4", className)}> <div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-col gap-0.5"> <h2 className="text-lg font-semibold">{model.name}</h2> <p className="text-sm text-muted-foreground"> {model.structures.length} structures, {model.views.length} views </p> </div> <Select items={models.map(({ name }) => ({ label: name, value: name }))} value={model.name} onValueChange={(next) => { if (!next) return setOrgan(next) setSelectedId(null) }} > <SelectTrigger aria-label="Organ" className="min-w-40"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectGroup> {models.map(({ name }) => ( <SelectItem key={name} value={name}> {name} </SelectItem> ))} </SelectGroup> </SelectContent> </Select> </div> <OrganViewer key={model.name} model={model} selectedId={selectedId} onSelectionChange={setSelectedId} className="h-[28rem]" /> <p className="text-sm text-muted-foreground" role="status"> {selected ? `${selected.label}: ${selected.description ?? selected.group}` : `Select a structure of the ${model.name.toLowerCase()} to learn about it.`} </p> </div> ) }