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.tsx
    import { 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.tsx
    import { 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.tsx
    import { 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>
      )
    }