{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "api-endpoint",
  "title": "API Endpoint",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "An endpoint reference card: method and path with highlighted parameters, auth, grouped parameter lists, and request and response examples in tabs with copy.",
  "dependencies": [
    "lucide-react",
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/copy-button.json",
    "https://ui.ballmac.com/r/highlight.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/api-endpoint.tsx",
      "content": "// Ballmac UI: API Endpoint. https://ui.ballmac.com/components/api-endpoint\n\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown, Lock } from \"lucide-react\"\nimport { Collapsible as CollapsiblePrimitive, Tabs as TabsPrimitive } from \"radix-ui\"\n\nimport { CopyButton } from \"@/components/ballmac/copy-button\"\nimport { highlightLines, tokenClass, type HighlightLanguage } from \"@/lib/ballmac/highlight\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\"\n\ntype EndpointParameter = {\n  /** Parameter name. */\n  name: string\n  /** Where it goes. */\n  in: \"path\" | \"query\" | \"header\" | \"body\"\n  /** Type shown in mono, such as \"string\" or \"integer\". */\n  type: string\n  /** The caller must send it. */\n  required?: boolean\n  /** What it means. */\n  description?: string\n  /** Default value when omitted. */\n  default?: string\n}\n\ntype EndpointResponse = {\n  /** HTTP status code. */\n  status: number\n  /** Short meaning, such as \"Customer created\". */\n  description: string\n  /** Example body, usually JSON. */\n  example?: string\n  /** Highlighter for the example. */\n  language?: HighlightLanguage\n}\n\nconst METHOD_STYLE: Record<HttpMethod, string> = {\n  GET: \"border-chart-2/40 bg-chart-2/10\",\n  POST: \"border-chart-1/40 bg-chart-1/10\",\n  PUT: \"border-chart-3/40 bg-chart-3/10\",\n  PATCH: \"border-chart-3/40 bg-chart-3/10\",\n  DELETE: \"border-destructive/40 bg-destructive/10\",\n}\n\nfunction statusMeaning(code: number) {\n  if (code < 300) return \"Success\"\n  if (code < 400) return \"Redirect\"\n  if (code < 500) return \"Client error\"\n  return \"Server error\"\n}\n\nfunction statusDot(code: number) {\n  return code < 300 ? \"bg-chart-2\" : code < 400 ? \"bg-chart-1\" : code < 500 ? \"bg-chart-3\" : \"bg-destructive\"\n}\n\n/** Splits \"/v1/customers/{id}\" so path parameters can be styled. */\nfunction pathParts(path: string) {\n  return path.split(/(\\{[^}]+\\})/g).filter(Boolean)\n}\n\nfunction Code({ code, language, label }: { code: string; language: HighlightLanguage; label: string }) {\n  const lines = React.useMemo(() => highlightLines(code, language), [code, language])\n  return (\n    <div className=\"relative\">\n      <pre dir=\"ltr\"\n        role=\"region\"\n        aria-label={label}\n        tabIndex={0}\n        className=\"max-h-72 overflow-auto rounded-lg border bg-muted/40 p-3.5 font-mono text-xs leading-5 text-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n      >\n        <code className=\"grid\">\n          {lines.map((line, i) => (\n            <span key={i} className=\"whitespace-pre\">\n              {line.length === 0 ? \" \" : line.map((t, j) => <span key={j} className={tokenClass[t.type]}>{t.text}</span>)}\n            </span>\n          ))}\n        </code>\n      </pre>\n      <CopyButton size=\"sm\" variant=\"outline\" value={code} ariaLabel={`Copy ${label}`} className=\"absolute top-2 end-2 bg-background/90 backdrop-blur\" />\n    </div>\n  )\n}\n\ntype ApiEndpointProps = Omit<React.ComponentProps<\"div\">, \"title\" | \"children\"> & {\n  /** HTTP method. */\n  method: HttpMethod\n  /** Path, with `{name}` for path parameters. */\n  path: string\n  /** Short name of the operation, such as \"Create a customer\". */\n  summary: string\n  /** Longer explanation. */\n  description?: string\n  /** Base URL used by the \"Copy URL\" button. */\n  baseUrl?: string\n  /** Authentication needed, such as \"Bearer token\". Shown as a lock chip. */\n  auth?: string\n  /** Request fields. */\n  parameters?: EndpointParameter[]\n  /** Example request body. */\n  requestExample?: string\n  /** Highlighter for the request body. */\n  requestLanguage?: HighlightLanguage\n  /** Possible responses. */\n  responses?: EndpointResponse[]\n  /** Open at first. */\n  defaultOpen?: boolean\n  /** Controlled open state. */\n  open?: boolean\n  /** Called when the card opens or closes. */\n  onOpenChange?: (open: boolean) => void\n}\n\nconst GROUPS: EndpointParameter[\"in\"][] = [\"path\", \"query\", \"header\", \"body\"]\n\nfunction ApiEndpoint({\n  method,\n  path,\n  summary,\n  description,\n  baseUrl,\n  auth,\n  parameters = [],\n  requestExample,\n  requestLanguage = \"json\",\n  responses = [],\n  defaultOpen = true,\n  open,\n  onOpenChange,\n  className,\n  ...props\n}: ApiEndpointProps) {\n  const msg = useMessages()\n  const panelId = React.useId()\n  const tabs = [\n    ...(requestExample ? [{ id: \"request\", label: \"Request\", dot: \"bg-muted-foreground\" }] : []),\n    ...responses.map((r) => ({ id: String(r.status), label: String(r.status), dot: statusDot(r.status) })),\n  ]\n  const [tab, setTab] = React.useState(tabs[0]?.id ?? \"\")\n  const url = `${(baseUrl ?? \"\").replace(/\\/$/, \"\")}${path}`\n\n  return (\n    <CollapsiblePrimitive.Root\n      data-slot=\"api-endpoint\"\n      defaultOpen={defaultOpen}\n      open={open}\n      onOpenChange={onOpenChange}\n      className={cn(\"w-full overflow-hidden rounded-xl border bg-card text-card-foreground shadow-xs\", className)}\n      {...props}\n    >\n      <div className=\"flex items-center gap-1 pe-2\">\n        <CollapsiblePrimitive.Trigger\n          aria-controls={panelId}\n          className=\"group flex min-h-14 min-w-0 flex-1 items-center gap-3 px-4 py-2.5 text-start outline-none transition-colors hover:bg-accent/40 focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50 motion-reduce:transition-none\"\n        >\n          <span\n            className={cn(\n              \"inline-flex h-6 min-w-14 shrink-0 items-center justify-center rounded-md border px-2 font-mono text-[11px] font-bold tracking-wide text-foreground\",\n              METHOD_STYLE[method]\n            )}\n          >\n            {method}\n          </span>\n          <span className=\"min-w-0 flex-1\">\n            <span className=\"block truncate font-mono text-[13px] font-medium text-foreground\">\n              {pathParts(path).map((part, i) =>\n                part.startsWith(\"{\") ? (\n                  <span key={i} className=\"-mx-px rounded-[4px] bg-muted px-0.5 text-foreground\">\n                    {part}\n                  </span>\n                ) : (\n                  <React.Fragment key={i}>{part}</React.Fragment>\n                )\n              )}\n            </span>\n            <span className=\"block truncate text-xs text-muted-foreground\">{summary}</span>\n          </span>\n          {auth && (\n            <span className=\"hidden shrink-0 items-center gap-1 rounded-full border px-2 py-0.5 text-xs text-foreground sm:inline-flex\">\n              <Lock aria-hidden=\"true\" className=\"size-3\" />\n              {auth}\n            </span>\n          )}\n          <ChevronDown\n            aria-hidden=\"true\"\n            className=\"size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180 motion-reduce:transition-none\"\n          />\n        </CollapsiblePrimitive.Trigger>\n        <CopyButton size=\"sm\" value={url} ariaLabel={`Copy URL for ${method} ${path}`} />\n      </div>\n      <CollapsiblePrimitive.Content\n        id={panelId}\n        className=\"overflow-hidden data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 motion-reduce:animate-none\"\n      >\n        <div className=\"grid gap-5 border-t p-4 @container\">\n          {description && <p className=\"text-sm leading-6 text-muted-foreground\">{description}</p>}\n          {auth && (\n            <p className=\"inline-flex w-fit items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs text-foreground sm:hidden\">\n              <Lock aria-hidden=\"true\" className=\"size-3\" />\n              {auth}\n            </p>\n          )}\n\n          {GROUPS.map((group) => {\n            const items = parameters.filter((p) => p.in === group)\n            if (items.length === 0) return null\n            return (\n              <section key={group} aria-label={msg(\"api-endpoint.parameters\", \"{group} parameters\", { group })}>\n                <h4 className=\"mb-1.5 text-xs font-semibold tracking-wide text-muted-foreground uppercase\">\n                  {group === \"body\" ? \"Body\" : `${group[0]!.toUpperCase()}${group.slice(1)} parameters`}\n                </h4>\n                <ul className=\"divide-y rounded-lg border\">\n                  {items.map((p) => (\n                    <li key={p.name} className=\"grid gap-1 px-3 py-2.5 @md:grid-cols-[minmax(0,13rem)_1fr] @md:gap-4\">\n                      <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n                        <code className=\"font-mono text-[13px] font-medium text-foreground\">{p.name}</code>\n                        <span className=\"font-mono text-xs text-muted-foreground\">{p.type}</span>\n                        {p.required && (\n                          <span className=\"rounded-full border border-destructive/40 bg-destructive/10 px-1.5 text-[11px] leading-4 font-medium text-foreground\">\n                            required\n                          </span>\n                        )}\n                      </div>\n                      <p className=\"text-[13px] leading-5 text-muted-foreground\">\n                        {p.description}\n                        {p.default !== undefined && (\n                          <span>\n                            {p.description ? \" \" : \"\"}{msg.rich(\"api-endpoint.defaultValue\", \"Default: {value}\", { value: <code className=\"font-mono text-foreground\">{p.default}</code> })}\n                          </span>\n                        )}\n                      </p>\n                    </li>\n                  ))}\n                </ul>\n              </section>\n            )\n          })}\n\n          {tabs.length > 0 && (\n            <TabsPrimitive.Root value={tab} onValueChange={setTab} className=\"grid gap-2.5\">\n              <TabsPrimitive.List aria-label={msg(\"api-endpoint.requestAndResponses\", \"Request and responses\")} className=\"flex flex-wrap items-center gap-1\">\n                {tabs.map((t) => (\n                  <TabsPrimitive.Trigger\n                    key={t.id}\n                    value={t.id}\n                    className=\"inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 font-mono text-xs font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[state=active]:bg-muted data-[state=active]:text-foreground motion-reduce:transition-none\"\n                  >\n                    <span aria-hidden=\"true\" className={cn(\"size-1.5 rounded-full\", t.dot)} />\n                    {t.label}\n                  </TabsPrimitive.Trigger>\n                ))}\n              </TabsPrimitive.List>\n              {requestExample && (\n                <TabsPrimitive.Content value=\"request\" className=\"grid gap-2 rounded-lg outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\">\n                  <Code code={requestExample} language={requestLanguage} label=\"request body\" />\n                </TabsPrimitive.Content>\n              )}\n              {responses.map((r) => (\n                <TabsPrimitive.Content key={r.status} value={String(r.status)} className=\"grid gap-2 rounded-lg outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\">\n                  <p className=\"text-[13px] text-foreground\">\n                    <span className=\"font-medium\">{statusMeaning(r.status)}.</span>{\" \"}\n                    <span className=\"text-muted-foreground\">{r.description}</span>\n                  </p>\n                  {r.example && <Code code={r.example} language={r.language ?? \"json\"} label={`${r.status} response body`} />}\n                </TabsPrimitive.Content>\n              ))}\n            </TabsPrimitive.Root>\n          )}\n        </div>\n      </CollapsiblePrimitive.Content>\n    </CollapsiblePrimitive.Root>\n  )\n}\n\nexport { ApiEndpoint, type ApiEndpointProps, type EndpointParameter, type EndpointResponse, type HttpMethod }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/api-endpoint.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "api",
      "endpoint",
      "docs",
      "rest",
      "reference"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "method, path (use {id} for path parameters), summary, description, auth, parameters [{ name, in, type, required, description }], requestExample and responses [{ status, description, example }]. Collapsible; copies the full URL when baseUrl is set.",
      "whenToUse": [
        "Hand-written API reference pages",
        "Internal docs for a small API"
      ],
      "whenNotToUse": [
        "Generating a full OpenAPI portal (render from the spec with your own loop over this component)"
      ],
      "composesWith": [
        "snippet-tabs",
        "code-block",
        "json-viewer"
      ],
      "a11y": [
        {
          "keys": "Enter / Space",
          "action": "Opens or closes the card"
        },
        {
          "keys": "ArrowLeft / ArrowRight",
          "action": "Moves between Request and response tabs"
        },
        {
          "keys": "Screen readers",
          "action": "Parameters are grouped lists; each example is a labelled region; status tabs say Success or error class"
        }
      ],
      "customization": [
        "defaultOpen / open",
        "baseUrl",
        "auth",
        "per-response language"
      ]
    },
    "examples": [
      "api-endpoint-demo",
      "api-endpoint-list"
    ],
    "url": "https://ui.ballmac.com/components/api-endpoint"
  },
  "categories": [
    "developer"
  ],
  "type": "registry:ui"
}