{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "side-panel",
  "title": "Side panel",
  "description": "The app's drawer: a right-hand panel on desktop and a bottom sheet on phones, with a labelled field and a pill segmented control.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "src/components/ui/side-panel.tsx",
      "content": "\"use client\";\n\nimport { useEffect, type ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\n\n/**\n * The app's settings drawer: a scrim plus a right-hand panel that slides in,\n * closes on Escape or a scrim click, and scrolls its own body.\n *\n * Shared so every \"configure this screen\" surface behaves identically — the\n * trainer's Settings and the Explorer's solution picker are the same gesture and\n * should not drift apart.\n */\nexport function SidePanel({\n  title,\n  onClose,\n  children,\n  footer,\n  size = \"default\",\n  bodyClassName = \"flex flex-col gap-6 px-5 py-5\",\n}: {\n  title: string;\n  onClose: () => void;\n  children: ReactNode;\n  /** Optional pinned row under the scrollable body. */\n  footer?: ReactNode;\n  /**\n   * `default` is the narrow settings drawer. `wide` is for pickers with a lot\n   * to show: a near-full-screen bottom drawer on phones, and a wide right-hand\n   * panel (up to 64rem) on larger screens.\n   */\n  size?: \"default\" | \"wide\";\n  /** Layout classes for the scrollable body (defaults to a padded column). */\n  bodyClassName?: string;\n}) {\n  useEffect(() => {\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") onClose();\n    };\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [onClose]);\n\n  const wide = size === \"wide\";\n  return (\n    <div\n      role=\"dialog\"\n      aria-modal=\"true\"\n      aria-label={title}\n      className={`fixed inset-0 z-50 flex bg-black/60 ${\n        wide ? \"items-end sm:items-stretch sm:justify-end\" : \"justify-end\"\n      }`}\n      onClick={onClose}\n    >\n      <aside\n        onClick={(e) => e.stopPropagation()}\n        className={`animate-in flex w-full flex-col bg-card duration-200 ${\n          wide\n            ? \"slide-in-from-bottom h-[94dvh] rounded-t-2xl border-t border-border sm:slide-in-from-right sm:h-full sm:max-w-5xl sm:rounded-none sm:border-l sm:border-t-0\"\n            : \"slide-in-from-right h-full max-w-md border-l border-border\"\n        }`}\n      >\n        <header className=\"flex shrink-0 items-center justify-between border-b border-border px-5 py-4\">\n          <h2 className=\"text-base font-semibold text-foreground\">{title}</h2>\n          <button\n            type=\"button\"\n            onClick={onClose}\n            aria-label=\"Close\"\n            className=\"rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground\"\n          >\n            <X className=\"size-4\" />\n          </button>\n        </header>\n        <div className={`min-h-0 flex-1 overflow-y-auto ${bodyClassName}`}>{children}</div>\n        {footer ? <div className=\"shrink-0 border-t border-border px-5 py-3\">{footer}</div> : null}\n      </aside>\n    </div>\n  );\n}\n\n/** One labelled setting inside a <SidePanel/>. */\nexport function PanelField({ label, children }: { label: string; children: ReactNode }) {\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <span className=\"text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground\">\n        {label}\n      </span>\n      {children}\n    </div>\n  );\n}\n\n/** The app's pill segmented control: a filled pill marks the active option. */\nexport function Segmented<T extends string>({\n  options,\n  value,\n  onChange,\n  wrap = false,\n}: {\n  options: { value: T; label: string }[];\n  value: T;\n  onChange: (value: T) => void;\n  /** Let the track wrap onto multiple lines (long option lists, e.g. stacks). */\n  wrap?: boolean;\n}) {\n  return (\n    <div className={`inline-flex gap-1 rounded-full bg-secondary p-1 ${wrap ? \"flex-wrap\" : \"w-fit\"}`}>\n      {options.map((opt) => {\n        const active = opt.value === value;\n        return (\n          <button\n            key={opt.value}\n            type=\"button\"\n            onClick={() => onChange(opt.value)}\n            aria-pressed={active}\n            className={`rounded-full px-3.5 py-1.5 text-xs font-semibold tabular-nums transition-colors ${\n              active\n                ? \"bg-primary text-primary-foreground\"\n                : \"text-muted-foreground hover:text-foreground\"\n            }`}\n          >\n            {opt.label}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/ui/side-panel.tsx"
    }
  ],
  "type": "registry:component"
}