{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "api-endpoint",
  "title": "API Endpoint",
  "description": "A collapsible API reference card with a monochrome-safe method badge, grouped parameter tables, expandable response examples, and a generated Copy-as-cURL command that reports failure honestly.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/api-endpoint.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronRight, Copy, Lock, TriangleAlert } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type ApiEndpointMethod = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\"\n\nexport type ApiEndpointParamLocation = \"path\" | \"query\" | \"header\" | \"body\"\n\nexport interface ApiEndpointParam {\n  /** Parameter name. Path params must match a `{name}` placeholder in `path` to be substituted. */\n  name: string\n  /** Free-form type label (\"string\", \"integer\", \"string[]\"…). Also used as the cURL placeholder when no `example` is given. */\n  type: string\n  /** Which part of the request the parameter belongs to — drives both the table grouping and the cURL builder. */\n  in: ApiEndpointParamLocation\n  required?: boolean\n  description?: React.ReactNode\n  /**\n   * Literal example value. In body payloads it is `JSON.parse`d first, so\n   * `\"42\"` / `\"true\"` / `'{\"a\":1}'` become real JSON values and anything that\n   * fails to parse is emitted as a JSON string.\n   */\n  example?: string\n}\n\nexport interface ApiEndpointResponse {\n  /** Status code — `200`, `\"200\"` and `\"4XX\"` are all accepted. */\n  status: string | number\n  description: string\n  /** Pretty-printed payload. Its presence is what turns the row into an expandable disclosure. */\n  example?: string\n}\n\nexport interface ApiEndpointAuth {\n  /** Short chip label next to the path, e.g. \"Bearer token\". */\n  label: string\n  /**\n   * Full header line copied *verbatim* into the generated cURL, e.g.\n   * `Authorization: Bearer $API_KEY`. Left out, auth stays out of the command.\n   */\n  header?: string\n  description?: React.ReactNode\n}\n\nexport interface ApiEndpointProps extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n  method: ApiEndpointMethod\n  /** Route with `{param}` placeholders, e.g. `/v1/customers/{id}`. Placeholders are highlighted. */\n  path: string\n  /** One-line human title of the operation. */\n  summary: string\n  description?: React.ReactNode\n  params?: ApiEndpointParam[]\n  responses?: ApiEndpointResponse[]\n  auth?: ApiEndpointAuth\n  /** Origin prefixed to `path` in the generated cURL. */\n  baseUrl?: string\n  /** Start expanded. Uncontrolled — later changes are ignored. */\n  defaultOpen?: boolean\n}\n\n/** Stable empty defaults: inline `[]` defaults would break every useMemo below. */\nconst NO_PARAMS: ApiEndpointParam[] = []\nconst NO_RESPONSES: ApiEndpointResponse[] = []\n\n/**\n * Five visually distinct treatments that survive a monochrome palette: tint /\n * solid / outline / dashed outline / destructive. Chart tokens are deliberately\n * avoided — they are fills, and score ~1.3:1 as text on this theme.\n */\nconst METHOD_BADGE: Record<ApiEndpointMethod, string> = {\n  GET: \"border-transparent bg-primary/10 text-primary\",\n  POST: \"border-transparent bg-primary text-primary-foreground\",\n  PUT: \"border-primary/40 bg-transparent text-primary\",\n  PATCH: \"border-dashed border-primary/50 bg-transparent text-primary\",\n  DELETE: \"border-destructive/40 bg-destructive/10 text-destructive\",\n}\n\nconst PARAM_GROUPS: { in: ApiEndpointParamLocation; label: string }[] = [\n  { in: \"path\", label: \"Path parameters\" },\n  { in: \"query\", label: \"Query parameters\" },\n  { in: \"header\", label: \"Header parameters\" },\n  { in: \"body\", label: \"Body parameters\" },\n]\n\nconst COPY_RESET_DELAY = 2000\n\nfunction statusTone(status: string) {\n  if (status.startsWith(\"2\")) return \"border-transparent bg-primary/10 text-primary\"\n  if (status.startsWith(\"4\") || status.startsWith(\"5\")) return \"border-transparent bg-destructive/10 text-destructive\"\n  return \"border-transparent bg-muted text-muted-foreground\"\n}\n\n/** Escape a value that will sit inside a double-quoted shell word. */\nfunction escapeDoubleQuoted(value: string) {\n  return value.replace(/([\"\\\\$`])/g, \"\\\\$1\")\n}\n\n/** Body values go through JSON.parse first so numbers/booleans/objects stay unquoted. */\nfunction bodyValue(param: ApiEndpointParam): unknown {\n  if (param.example === undefined) return `<${param.type}>`\n  try {\n    return JSON.parse(param.example) as unknown\n  } catch {\n    return param.example\n  }\n}\n\nfunction buildCurl(\n  method: ApiEndpointMethod,\n  path: string,\n  baseUrl: string,\n  params: ApiEndpointParam[],\n  auth: ApiEndpointAuth | undefined,\n) {\n  const at = (location: ApiEndpointParamLocation) => params.filter(param => param.in === location)\n  const pathParams = at(\"path\")\n\n  // Substitute `{id}` with its example; keep the placeholder when there is none\n  // so the reader can see exactly what still has to be filled in.\n  const filledPath = path.replace(/\\{([^{}]+)\\}/g, (placeholder, name: string) => {\n    const match = pathParams.find(param => param.name === name)\n    return match?.example === undefined ? placeholder : encodeURIComponent(match.example)\n  })\n\n  const query = at(\"query\").filter(param => param.required || param.example !== undefined)\n  const search = query\n    .map(param =>\n      param.example === undefined\n        ? `${encodeURIComponent(param.name)}=<${param.name}>`\n        : `${encodeURIComponent(param.name)}=${encodeURIComponent(param.example)}`,\n    )\n    .join(\"&\")\n\n  const origin = baseUrl.replace(/\\/+$/, \"\")\n  const separator = filledPath.startsWith(\"/\") ? \"\" : \"/\"\n  const url = `${origin}${separator}${filledPath}${search ? `?${search}` : \"\"}`\n\n  // -X is always explicit: a -d payload must never silently promote the request\n  // to POST.\n  const lines = [`curl -X ${method} \"${escapeDoubleQuoted(url)}\"`]\n\n  // Written verbatim so shell variables like $API_KEY survive the copy.\n  if (auth?.header) lines.push(`-H \"${auth.header}\"`)\n\n  for (const header of at(\"header\").filter(param => param.required || param.example !== undefined)) {\n    const value = header.example === undefined ? `<${header.type}>` : escapeDoubleQuoted(header.example)\n    lines.push(`-H \"${escapeDoubleQuoted(header.name)}: ${value}\"`)\n  }\n\n  const body = at(\"body\")\n  if (body.length > 0) {\n    const payload: Record<string, unknown> = {}\n    for (const param of body) payload[param.name] = bodyValue(param)\n    lines.push(`-H \"Content-Type: application/json\"`)\n    lines.push(`-d '${JSON.stringify(payload, null, 2).replace(/'/g, `'\\\\''`)}'`)\n  }\n\n  return lines.join(\" \\\\\\n  \")\n}\n\nfunction PathText({ path }: { path: string }) {\n  return (\n    <>\n      {path.split(/(\\{[^{}]+\\})/g).map((part, index) =>\n        part.startsWith(\"{\") && part.endsWith(\"}\") ? (\n          <span className=\"rounded-sm bg-primary/10 px-0.5 text-primary\" key={`${part}-${index}`}>\n            {part}\n          </span>\n        ) : (\n          <React.Fragment key={`${part}-${index}`}>{part}</React.Fragment>\n        ),\n      )}\n    </>\n  )\n}\n\nfunction ParamTable({ label, caption, items }: { label: string; caption: string; items: ApiEndpointParam[] }) {\n  return (\n    <section className=\"min-w-0\">\n      <h4 className=\"text-xs font-semibold uppercase tracking-wide text-muted-foreground\">{label}</h4>\n      <div className=\"mt-2 overflow-x-auto\">\n        {/* min-w keeps Description readable once Name and Type take their natural widths; below\n            it the wrapper above scrolls horizontally rather than shrinking the columns. */}\n        <table className=\"w-full min-w-[32rem] border-collapse text-left\">\n          <caption className=\"sr-only\">{caption}</caption>\n          <thead>\n            <tr className=\"border-b\">\n              <th className=\"py-2 pr-4 text-xs font-medium uppercase tracking-wide text-muted-foreground\" scope=\"col\">\n                Name\n              </th>\n              <th className=\"py-2 pr-4 text-xs font-medium uppercase tracking-wide text-muted-foreground\" scope=\"col\">\n                Type\n              </th>\n              <th className=\"py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground\" scope=\"col\">\n                Description\n              </th>\n            </tr>\n          </thead>\n          <tbody>\n            {items.map((param, index) => (\n              <tr className=\"border-b align-top last:border-b-0\" key={`${param.name}-${index}`}>\n                <th className=\"py-2 pr-4 font-normal\" scope=\"row\">\n                  {/* break-words, not break-all: an identifier only splits when it cannot fit a\n                      line at all, so the column keeps its word as its minimum width instead of\n                      collapsing to one character and shredding every name. */}\n                  <code className=\"break-words font-mono text-sm font-medium\">{param.name}</code>\n                  {param.required && (\n                    <span className=\"mt-0.5 block text-xs font-medium text-destructive\">required</span>\n                  )}\n                </th>\n                {/* type labels are short tokens — never wrapped, so the column hugs them and the\n                    slack goes to Description */}\n                <td className=\"py-2 pr-4 whitespace-nowrap\">\n                  <code className=\"font-mono text-xs text-muted-foreground\">{param.type}</code>\n                </td>\n                <td className=\"py-2 text-sm text-muted-foreground\">{param.description}</td>\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      </div>\n    </section>\n  )\n}\n\nfunction ResponseRow({ response }: { response: ApiEndpointResponse }) {\n  const uid = React.useId()\n  const [open, setOpen] = React.useState(false)\n  const panelId = `${uid}-response`\n  const status = String(response.status)\n  const chip = (\n    <span\n      className={cn(\n        \"rounded-md border px-2 py-0.5 font-mono text-xs font-semibold tabular-nums\",\n        statusTone(status),\n      )}\n    >\n      {status}\n    </span>\n  )\n\n  // No example → no disclosure. A row that cannot expand never renders as a button.\n  if (!response.example) {\n    return (\n      <li className=\"flex flex-wrap items-center gap-3 px-3 py-2.5\">\n        <span aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n        {chip}\n        <span className=\"min-w-0 break-words text-sm text-muted-foreground\">{response.description}</span>\n      </li>\n    )\n  }\n\n  return (\n    <li>\n      <button\n        aria-controls={panelId}\n        aria-expanded={open}\n        className=\"flex w-full cursor-pointer flex-wrap items-center gap-3 px-3 py-2.5 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none\"\n        onClick={() => setOpen(value => !value)}\n        type=\"button\"\n      >\n        <ChevronRight\n          aria-hidden=\"true\"\n          className={cn(\n            \"size-4 shrink-0 text-muted-foreground transition-transform duration-200 motion-reduce:transition-none\",\n            open && \"rotate-90\",\n          )}\n        />\n        {chip}\n        <span className=\"min-w-0 break-words text-sm text-muted-foreground\">{response.description}</span>\n      </button>\n      <div className=\"px-3 pb-3\" hidden={!open} id={panelId}>\n        <pre className=\"overflow-x-auto rounded-md border bg-muted/40 p-3 text-xs leading-relaxed\">\n          <code className=\"font-mono\">{response.example}</code>\n        </pre>\n      </div>\n    </li>\n  )\n}\n\nexport const ApiEndpoint = React.forwardRef<HTMLElement, ApiEndpointProps>(\n  (\n    {\n      method,\n      path,\n      summary,\n      description,\n      params = NO_PARAMS,\n      responses = NO_RESPONSES,\n      auth,\n      baseUrl = \"https://api.example.com\",\n      defaultOpen = false,\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const uid = React.useId()\n    const triggerId = `${uid}-trigger`\n    const panelId = `${uid}-panel`\n\n    const [open, setOpen] = React.useState(defaultOpen)\n    const [copyState, setCopyState] = React.useState<\"idle\" | \"copied\" | \"failed\">(\"idle\")\n    const resetTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n    const frameRef = React.useRef<number | null>(null)\n    const fallbackRef = React.useRef<HTMLPreElement>(null)\n    const mountedRef = React.useRef(true)\n\n    const curl = React.useMemo(\n      () => buildCurl(method, path, baseUrl, params, auth),\n      [method, path, baseUrl, params, auth],\n    )\n    const groups = React.useMemo(\n      () =>\n        PARAM_GROUPS.map(group => ({\n          ...group,\n          items: params.filter(param => param.in === group.in),\n        })).filter(group => group.items.length > 0),\n      [params],\n    )\n\n    // Re-armed on mount: StrictMode replays mount → cleanup → mount in dev, and a\n    // flag only ever set to false would stay false for the surviving instance.\n    React.useEffect(() => {\n      mountedRef.current = true\n      return () => {\n        mountedRef.current = false\n        if (resetTimerRef.current !== null) clearTimeout(resetTimerRef.current)\n        if (frameRef.current !== null) cancelAnimationFrame(frameRef.current)\n      }\n    }, [])\n\n    const handleCopy = () => {\n      if (resetTimerRef.current !== null) {\n        clearTimeout(resetTimerRef.current)\n        resetTimerRef.current = null\n      }\n\n      // Insecure context / blocked permission / unsupported browser: say so and\n      // fall back to a selectable command. Never report a copy that never happened.\n      const fail = () => {\n        // A rejection can land after the card has gone (route change mid-write).\n        if (!mountedRef.current) return\n        setCopyState(\"failed\")\n        if (frameRef.current !== null) cancelAnimationFrame(frameRef.current)\n        frameRef.current = requestAnimationFrame(() => {\n          frameRef.current = null\n          const node = fallbackRef.current\n          if (node) window.getSelection()?.selectAllChildren(node)\n        })\n      }\n\n      if (typeof navigator === \"undefined\" || !navigator.clipboard?.writeText) {\n        fail()\n        return\n      }\n\n      navigator.clipboard.writeText(curl).then(() => {\n        if (!mountedRef.current) return\n        setCopyState(\"copied\")\n        // Only the success chip auto-resets — the failure fallback has to stay\n        // on screen long enough to actually copy the command by hand.\n        resetTimerRef.current = setTimeout(() => {\n          setCopyState(\"idle\")\n          resetTimerRef.current = null\n        }, COPY_RESET_DELAY)\n      }, fail)\n    }\n\n    const CopyIcon = copyState === \"copied\" ? Check : copyState === \"failed\" ? TriangleAlert : Copy\n    const copyLabel = copyState === \"copied\" ? \"Copied\" : copyState === \"failed\" ? \"Copy failed\" : \"Copy as cURL\"\n    const hasBody = Boolean(description) || Boolean(auth?.description) || groups.length > 0 || responses.length > 0\n\n    return (\n      <article\n        aria-label={`${method} ${path}`}\n        className={cn(\"w-full min-w-0 overflow-hidden rounded-xl border bg-card text-card-foreground\", className)}\n        ref={ref}\n        {...props}\n      >\n        <div className=\"flex items-start gap-2 p-3 sm:p-4\">\n          <h3 className=\"min-w-0 flex-1\">\n            <button\n              aria-controls={panelId}\n              aria-expanded={open}\n              className=\"flex w-full cursor-pointer items-start gap-3 rounded-lg p-1 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\"\n              id={triggerId}\n              onClick={() => setOpen(value => !value)}\n              type=\"button\"\n            >\n              <ChevronRight\n                aria-hidden=\"true\"\n                className={cn(\n                  \"mt-1 size-4 shrink-0 text-muted-foreground transition-transform duration-200 motion-reduce:transition-none\",\n                  open && \"rotate-90\",\n                )}\n              />\n              <span className=\"flex min-w-0 flex-1 flex-col gap-1\">\n                <span className=\"flex flex-wrap items-center gap-2\">\n                  <span\n                    className={cn(\n                      \"rounded-md border px-2 py-0.5 font-mono text-xs font-semibold uppercase tracking-wide\",\n                      METHOD_BADGE[method],\n                    )}\n                  >\n                    {method}\n                  </span>\n                  <code className=\"min-w-0 break-all font-mono text-sm font-medium\">\n                    <PathText path={path} />\n                  </code>\n                  {auth && (\n                    <span className=\"inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs text-muted-foreground\">\n                      <Lock aria-hidden=\"true\" className=\"size-3\" />\n                      {auth.label}\n                    </span>\n                  )}\n                </span>\n                <span className=\"min-w-0 break-words text-sm text-muted-foreground\">{summary}</span>\n              </span>\n            </button>\n          </h3>\n          <button\n            aria-label=\"Copy as cURL\"\n            className=\"inline-flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md border px-2 py-1.5 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\"\n            onClick={handleCopy}\n            type=\"button\"\n          >\n            <CopyIcon\n              aria-hidden=\"true\"\n              className={cn(\"size-3.5\", copyState === \"failed\" && \"text-destructive\")}\n            />\n            <span className=\"hidden sm:inline\">{copyLabel}</span>\n          </button>\n          <span aria-live=\"polite\" className=\"sr-only\">\n            {copyState === \"copied\"\n              ? \"cURL command copied to clipboard\"\n              : copyState === \"failed\"\n                ? \"Copy failed. The command is shown below for manual copying.\"\n                : \"\"}\n          </span>\n        </div>\n\n        {copyState === \"failed\" && (\n          <div className=\"border-t px-3 py-3 sm:px-4\">\n            <p className=\"flex items-center gap-2 text-xs font-medium text-destructive\">\n              <TriangleAlert aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n              Clipboard unavailable — select the command below and copy it manually.\n            </p>\n            <pre\n              className=\"mt-2 overflow-x-auto rounded-md border bg-muted/40 p-3 text-xs leading-relaxed\"\n              ref={fallbackRef}\n            >\n              <code className=\"font-mono\">{curl}</code>\n            </pre>\n          </div>\n        )}\n\n        <div\n          aria-labelledby={triggerId}\n          className={cn(\n            \"grid transition-[grid-template-rows] duration-200 motion-reduce:transition-none\",\n            open ? \"grid-rows-[1fr]\" : \"grid-rows-[0fr]\",\n          )}\n          id={panelId}\n          inert={!open}\n          role=\"region\"\n        >\n          <div className=\"overflow-hidden\">\n            <div className=\"flex flex-col gap-6 border-t px-3 py-4 sm:px-4\">\n              {description && <p className=\"text-sm leading-relaxed text-muted-foreground\">{description}</p>}\n\n              {auth?.description && (\n                <div className=\"flex items-start gap-2 rounded-lg border bg-muted/40 px-3 py-2.5\">\n                  <Lock aria-hidden=\"true\" className=\"mt-0.5 size-3.5 shrink-0 text-muted-foreground\" />\n                  <div className=\"min-w-0 text-sm\">\n                    <span className=\"font-medium\">{auth.label}</span>\n                    <span className=\"block text-muted-foreground\">{auth.description}</span>\n                  </div>\n                </div>\n              )}\n\n              {groups.map(group => (\n                <ParamTable\n                  caption={`${group.label} for ${method} ${path}`}\n                  items={group.items}\n                  key={group.in}\n                  label={group.label}\n                />\n              ))}\n\n              {responses.length > 0 && (\n                <section className=\"min-w-0\">\n                  <h4 className=\"text-xs font-semibold uppercase tracking-wide text-muted-foreground\">Responses</h4>\n                  <ul className=\"mt-2 divide-y rounded-lg border\">\n                    {responses.map((response, index) => (\n                      <ResponseRow key={`${response.status}-${index}`} response={response} />\n                    ))}\n                  </ul>\n                </section>\n              )}\n\n              {!hasBody && (\n                <p className=\"text-sm text-muted-foreground\">\n                  No parameters or responses documented for this endpoint.\n                </p>\n              )}\n            </div>\n          </div>\n        </div>\n      </article>\n    )\n  },\n)\n\nApiEndpoint.displayName = \"ApiEndpoint\"\n\nexport default ApiEndpoint\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}