{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-explorer",
  "title": "Data Explorer",
  "description": "Query surface, results, saved views, and export actions.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "label",
    "table",
    "textarea",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json",
    "https://uaiblocks.dev/r/data-table-toolbar.json",
    "https://uaiblocks.dev/r/empty-state.json",
    "https://uaiblocks.dev/r/page-tabs.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/blocks/data-explorer.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { LoaderCircle, Play } from \"lucide-react\";\nimport { type ComponentProps, createContext, useContext, useEffect, useId, useRef } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport {\n  DataTableToolbar,\n  type DataTableToolbarProps,\n  type DataTableToolbarVariant,\n} from \"@/components/ui/uai/data-table-toolbar\";\nimport {\n  EmptyState,\n  type EmptyStateProps,\n  type EmptyStateVariant,\n} from \"@/components/ui/uai/empty-state\";\nimport { PageTabs, type PageTabsProps, type PageTabsVariant } from \"@/components/ui/uai/page-tabs\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const DATA_EXPLORER_VARIANTS = [\"workbench\", \"stacked\", \"compact\"] as const;\nexport type DataExplorerVariant = (typeof DATA_EXPLORER_VARIANTS)[number];\nexport type DataExplorerProps = ComponentProps<\"section\"> & {\n  variant?: DataExplorerVariant;\n  /** A query is running: the Run button reports busy and the status line shimmers. */\n  pending?: boolean;\n};\n\ntype ExplorerContext = { id: string; variant: DataExplorerVariant; pending: boolean };\nconst Context = createContext<ExplorerContext | null>(null);\nfunction useExplorer(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within DataExplorer`);\n  return context;\n}\n\nconst tabVariants: Record<DataExplorerVariant, PageTabsVariant> = {\n  workbench: \"underline\",\n  stacked: \"pill\",\n  compact: \"segmented\",\n};\nconst toolbarVariants: Record<DataExplorerVariant, DataTableToolbarVariant> = {\n  workbench: \"toolbar\",\n  stacked: \"toolbar\",\n  compact: \"compact\",\n};\nconst emptyVariants: Record<DataExplorerVariant, EmptyStateVariant> = {\n  workbench: \"plain\",\n  stacked: \"plain\",\n  compact: \"compact\",\n};\n\n// Fades whichever edge of the results still hides columns.\nconst scrollEdgeFade =\n  \"data-overflow-start:[mask-image:linear-gradient(to_right,transparent,black_24px)] data-overflow-end:[mask-image:linear-gradient(to_left,transparent,black_24px)] data-overflow-start:data-overflow-end:[mask-image:linear-gradient(to_right,transparent,black_24px,black_calc(100%-24px),transparent)]\";\nfunction useScrollEdges(ref: React.RefObject<HTMLElement | null>) {\n  useEffect(() => {\n    const element = ref.current;\n    if (!element) return;\n    const update = () => {\n      const max = element.scrollWidth - element.clientWidth;\n      element.toggleAttribute(\"data-overflow-start\", element.scrollLeft > 1);\n      element.toggleAttribute(\"data-overflow-end\", element.scrollLeft < max - 1);\n    };\n    update();\n    element.addEventListener(\"scroll\", update, { passive: true });\n    const observer = typeof ResizeObserver === \"function\" ? new ResizeObserver(update) : null;\n    observer?.observe(element);\n    for (const child of element.children) observer?.observe(child);\n    return () => {\n      element.removeEventListener(\"scroll\", update);\n      observer?.disconnect();\n    };\n  }, [ref]);\n}\n\nconst dataExplorerVariants = cva(\"grid min-w-0 content-start text-[13px]/[18px] text-foreground\", {\n  variants: {\n    variant: { workbench: \"gap-4\", stacked: \"gap-4\", compact: \"gap-2.5\" },\n  },\n});\n\nconst dataExplorerActionVariants = cva(\n  \"inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-full border-0 font-medium whitespace-nowrap transition-[filter,box-shadow,scale] duration-[120ms,120ms,140ms] ease-[ease-out,ease-out,cubic-bezier(0.23,1,0.32,1)] py-0 focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid enabled:active:scale-[0.97] disabled:pointer-events-auto disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none motion-reduce:enabled:active:scale-100 [&_svg:not([class*='size-'])]:size-3.5\",\n  {\n    variants: {\n      emphasis: {\n        primary:\n          \"bg-primary text-primary-foreground hover:bg-primary enabled:hover:shadow-none enabled:hover:brightness-108\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary enabled:hover:shadow-[inset_0_0_0_999px_color-mix(in_oklab,var(--foreground)_9%,transparent)]\",\n      },\n      compact: {\n        true: \"h-6.5 px-2.75 has-[>svg]:px-2.75 text-[12px]/4\",\n        false: \"h-7.5 px-3.25 has-[>svg]:px-3.25 text-[12.5px]/4\",\n      },\n    },\n  },\n);\n\n/** Query workspace: saved views, a query editor, and a results table. */\nexport function DataExplorer({\n  variant = \"workbench\",\n  pending = false,\n  className,\n  children,\n  ...props\n}: DataExplorerProps) {\n  const id = useId();\n  return (\n    <Context.Provider value={{ id, variant, pending }}>\n      <section\n        aria-labelledby={`${id}-title`}\n        data-slot=\"data-explorer\"\n        data-variant={variant}\n        className={cn(dataExplorerVariants({ variant }), className)}\n        {...props}\n      >\n        {children}\n      </section>\n    </Context.Provider>\n  );\n}\n\nexport function DataExplorerHeader({ className, ...props }: ComponentProps<\"div\">) {\n  useExplorer(\"DataExplorerHeader\");\n  return (\n    <div\n      data-slot=\"data-explorer-header\"\n      className={cn(\"flex min-w-0 flex-wrap items-end justify-between gap-3\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function DataExplorerHeading({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"data-explorer-heading\"\n      className={cn(\"grid min-w-0 flex-[1_1_240px] gap-1\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function DataExplorerTitle({ className, ...props }: ComponentProps<\"h2\">) {\n  const context = useExplorer(\"DataExplorerTitle\");\n  const compact = context.variant === \"compact\";\n  return (\n    <h2\n      data-slot=\"data-explorer-title\"\n      className={cn(\n        \"m-0 font-semibold tracking-[-0.015em] wrap-anywhere\",\n        compact ? \"text-[15px]/5\" : \"text-lg/6\",\n        className,\n      )}\n      {...props}\n      id={`${context.id}-title`}\n    />\n  );\n}\n\nexport function DataExplorerDescription({ className, ...props }: ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"data-explorer-description\"\n      className={cn(\"m-0 text-muted-foreground tabular-nums\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function DataExplorerActions({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"data-explorer-actions\"\n      className={cn(\"flex flex-wrap items-center gap-1.5\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function DataExplorerAction({\n  emphasis = \"secondary\",\n  type = \"button\",\n  className,\n  ...props\n}: ComponentProps<\"button\"> & { emphasis?: \"primary\" | \"secondary\" }) {\n  const context = useExplorer(\"DataExplorerAction\");\n  return (\n    <Button\n      data-slot=\"data-explorer-action\"\n      variant={emphasis === \"primary\" ? \"default\" : \"secondary\"}\n      data-emphasis={emphasis}\n      type={type}\n      className={cn(\n        dataExplorerActionVariants({ emphasis, compact: context.variant === \"compact\" }),\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** Saved views. Compose Page Tabs parts inside it. */\nexport function DataExplorerViews(props: Omit<PageTabsProps, \"variant\">) {\n  const context = useExplorer(\"DataExplorerViews\");\n  return <PageTabs {...props} variant={tabVariants[context.variant]} />;\n}\n\n/** Places the query beside the results in Workbench and above them otherwise. */\nexport function DataExplorerBody({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useExplorer(\"DataExplorerBody\");\n  return (\n    <div\n      data-slot=\"data-explorer-body\"\n      className={cn(\n        \"min-w-0 flex-wrap items-start\",\n        context.variant === \"workbench\" ? \"flex\" : \"grid\",\n        context.variant === \"compact\" ? \"gap-2\" : \"gap-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** Query form. Execution stays with the consumer through `onSubmit`. */\nexport function DataExplorerQuery({\n  \"aria-label\": label = \"Query\",\n  className,\n  ...props\n}: ComponentProps<\"form\">) {\n  const context = useExplorer(\"DataExplorerQuery\");\n  const compact = context.variant === \"compact\";\n  return (\n    <form\n      aria-label={label}\n      data-slot=\"data-explorer-query\"\n      className={cn(\n        \"m-0 grid min-w-0 flex-[1_1_260px] content-start border bg-card shadow-[0_1px_2px_oklch(0_0_0/0.04)]\",\n        compact ? \"gap-1.5 rounded-xl p-2.5\" : \"gap-2.5 rounded-[14px] p-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function DataExplorerQueryLabel({ className, children, ...props }: ComponentProps<\"label\">) {\n  const context = useExplorer(\"DataExplorerQueryLabel\");\n  return (\n    <Label\n      data-slot=\"data-explorer-query-label\"\n      className={cn(\n        \"block px-0.5 text-[11.5px]/4 font-medium text-subtle-foreground select-auto\",\n        className,\n      )}\n      {...props}\n      htmlFor={`${context.id}-query`}\n    >\n      {children}\n    </Label>\n  );\n}\n\nexport function DataExplorerQueryInput({\n  className,\n  ...props\n}: Omit<ComponentProps<\"textarea\">, \"id\">) {\n  const context = useExplorer(\"DataExplorerQueryInput\");\n  const compact = context.variant === \"compact\";\n  return (\n    <Textarea\n      spellCheck={false}\n      rows={compact ? 3 : 5}\n      data-slot=\"data-explorer-query-input\"\n      className={cn(\n        \"box-border block field-sizing-fixed min-h-0 w-full min-w-0 resize-y border-0 font-[family-name:var(--font-mono,ui-monospace,monospace)] text-inherit shadow-none transition-shadow duration-120 ease-out placeholder:text-subtle-foreground focus:shadow-[0_0_0_1px_var(--border-strong),0_0_0_4px_color-mix(in_oklab,var(--primary)_22%,transparent)] focus:outline-none focus-visible:ring-0 motion-reduce:transition-none\",\n        compact\n          ? \"rounded-lg px-2.5 py-2 text-[11.5px]/[17px] md:text-[11.5px]/[17px]\"\n          : \"rounded-[10px] px-3 py-2.5 text-[12px]/[19px] md:text-[12px]/[19px]\",\n        context.variant === \"workbench\"\n          ? \"bg-background dark:bg-background\"\n          : \"bg-[color-mix(in_oklab,var(--background)_60%,var(--card))] dark:bg-[color-mix(in_oklab,var(--background)_60%,var(--card))]\",\n        className,\n      )}\n      {...props}\n      id={`${context.id}-query`}\n    />\n  );\n}\n\nexport function DataExplorerRun({\n  children = \"Run query\",\n  className,\n  ...props\n}: Omit<ComponentProps<\"button\">, \"type\">) {\n  const context = useExplorer(\"DataExplorerRun\");\n  return (\n    <Button\n      data-slot=\"data-explorer-run\"\n      data-emphasis=\"primary\"\n      className={cn(\n        dataExplorerActionVariants({ emphasis: \"primary\", compact: context.variant === \"compact\" }),\n        \"justify-self-start\",\n        className,\n      )}\n      {...props}\n      type=\"submit\"\n      aria-busy={context.pending || undefined}\n      data-pending={context.pending || undefined}\n    >\n      {context.pending ? (\n        <LoaderCircle\n          strokeWidth={2.25}\n          aria-hidden=\"true\"\n          className=\"size-3 motion-safe:animate-spin\"\n        />\n      ) : (\n        <Play strokeWidth={2} fill=\"currentColor\" aria-hidden=\"true\" className=\"size-3\" />\n      )}\n      {children}\n    </Button>\n  );\n}\n\nexport function DataExplorerResults({\n  \"aria-label\": label = \"Results\",\n  className,\n  ...props\n}: ComponentProps<\"section\">) {\n  const context = useExplorer(\"DataExplorerResults\");\n  return (\n    <section\n      aria-label={label}\n      data-slot=\"data-explorer-results\"\n      className={cn(\n        \"grid min-w-0 flex-[999_1_380px] content-start\",\n        context.variant === \"compact\" ? \"gap-1.5\" : \"gap-2.5\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** Result controls such as export. Compose Data Table Toolbar parts inside it. */\nexport function DataExplorerToolbar(props: Omit<DataTableToolbarProps, \"variant\">) {\n  const context = useExplorer(\"DataExplorerToolbar\");\n  return <DataTableToolbar {...props} variant={toolbarVariants[context.variant]} />;\n}\n\n/** Scrollable results table. The wrapper is focusable so keyboard users can scroll it. */\nexport function DataExplorerTable({\n  \"aria-label\": label = \"Query results\",\n  className,\n  ...props\n}: ComponentProps<\"table\">) {\n  const context = useExplorer(\"DataExplorerTable\");\n  const compact = context.variant === \"compact\";\n  const scroller = useRef<HTMLElement>(null);\n  useScrollEdges(scroller);\n  return (\n    <section\n      ref={scroller}\n      aria-label={label}\n      // biome-ignore lint/a11y/noNoninteractiveTabindex: a scrollable region must be reachable by keyboard.\n      tabIndex={0}\n      data-slot=\"data-explorer-table-scroll\"\n      className={cn(\n        \"min-w-0 overflow-x-auto border bg-card shadow-[0_1px_2px_oklch(0_0_0/0.04)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring *:data-[slot=table-container]:overflow-visible\",\n        compact ? \"rounded-xl px-1 py-0.5\" : \"rounded-[14px] px-1.5 py-1\",\n        scrollEdgeFade,\n      )}\n    >\n      <Table\n        data-slot=\"data-explorer-table\"\n        className={cn(\n          \"w-full border-collapse\",\n          compact ? \"text-[12px]/[18px]\" : \"text-[13px]/[18px]\",\n          className,\n        )}\n        {...props}\n      />\n    </section>\n  );\n}\n\nexport function DataExplorerTableHead({ className, ...props }: ComponentProps<\"thead\">) {\n  return (\n    <TableHeader\n      data-slot=\"data-explorer-table-head\"\n      className={cn(\"bg-transparent [&_tr]:border-b-0\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function DataExplorerTableBody({ className, ...props }: ComponentProps<\"tbody\">) {\n  return (\n    <TableBody\n      data-slot=\"data-explorer-table-body\"\n      className={cn(\"[&_tr:last-child]:border-t\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function DataExplorerTableRow({ className, ...props }: ComponentProps<\"tr\">) {\n  return (\n    <TableRow\n      data-slot=\"data-explorer-table-row\"\n      className={cn(\n        \"border-t border-b-0 border-border/70 hover:bg-transparent [transition:background-color_120ms_ease-out] motion-reduce:transition-none [tbody>&]:animate-[enter_240ms_var(--ease-out-quint)_both] [tbody>&]:fade-in-0 [tbody>&]:slide-in-from-bottom-1 [tbody>&]:hover:bg-foreground/4 [tbody>&]:nth-2:[animation-delay:40ms] [tbody>&]:nth-3:[animation-delay:80ms] [tbody>&]:nth-4:[animation-delay:120ms] [tbody>&]:nth-5:[animation-delay:160ms] [tbody>&]:nth-[n+6]:[animation-delay:200ms] [tbody>&]:motion-reduce:animate-none [tbody>&>td:first-child]:font-medium\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function DataExplorerHeaderCell({\n  align = \"start\",\n  scope = \"col\",\n  className,\n  ...props\n}: Omit<ComponentProps<\"th\">, \"align\"> & { align?: \"start\" | \"end\" }) {\n  const context = useExplorer(\"DataExplorerHeaderCell\");\n  const compact = context.variant === \"compact\";\n  return (\n    <TableHead\n      scope={scope}\n      data-slot=\"data-explorer-header-cell\"\n      className={cn(\n        \"font-medium whitespace-nowrap text-subtle-foreground\",\n        compact ? \"h-7.5 px-2 text-[11.5px]\" : \"h-8.5 px-2.5 text-[12px]\",\n        align === \"end\" ? \"text-right\" : \"text-left\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function DataExplorerCell({\n  align = \"start\",\n  className,\n  ...props\n}: Omit<ComponentProps<\"td\">, \"align\"> & { align?: \"start\" | \"end\" }) {\n  const context = useExplorer(\"DataExplorerCell\");\n  return (\n    <TableCell\n      data-slot=\"data-explorer-cell\"\n      className={cn(\n        \"py-0 whitespace-nowrap\",\n        context.variant === \"compact\" ? \"h-8 px-2\" : \"h-9.5 px-2.5\",\n        align === \"end\" ? \"text-right tabular-nums\" : \"text-left\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** Row count, timing, or errors, announced politely. */\nexport function DataExplorerStatus({ className, ...props }: ComponentProps<\"p\">) {\n  const context = useExplorer(\"DataExplorerStatus\");\n  return (\n    <p\n      role=\"status\"\n      data-slot=\"data-explorer-status\"\n      data-pending={context.pending || undefined}\n      className={cn(\n        \"m-0 px-0.5 text-[12px] text-subtle-foreground tabular-nums\",\n        context.pending && \"shimmer-text\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** Shown when a query returns no rows. Compose Empty State parts inside it. */\nexport function DataExplorerEmpty(props: Omit<EmptyStateProps, \"variant\">) {\n  const context = useExplorer(\"DataExplorerEmpty\");\n  return <EmptyState {...props} variant={emptyVariants[context.variant]} />;\n}\n",
      "type": "registry:component",
      "target": "@components/uai/data-explorer.tsx"
    }
  ],
  "categories": [
    "operations",
    "data",
    "query"
  ],
  "type": "registry:block"
}