{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "approval-queue",
  "title": "Approval Queue",
  "description": "Pending decisions grouped by risk, age, owner, and impact.",
  "dependencies": [],
  "registryDependencies": [
    "button",
    "label",
    "radio-group",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json",
    "https://uaiblocks.dev/r/approval-card.json",
    "https://uaiblocks.dev/r/empty-state.json",
    "https://uaiblocks.dev/r/filter-bar.json",
    "https://uaiblocks.dev/r/metric-card.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/blocks/approval-queue.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentProps,\n  createContext,\n  type ReactNode,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport {\n  ApprovalCard,\n  type ApprovalCardProps,\n  type ApprovalCardVariant,\n} from \"@/components/ui/uai/approval-card\";\nimport {\n  EmptyState,\n  type EmptyStateProps,\n  type EmptyStateVariant,\n} from \"@/components/ui/uai/empty-state\";\nimport {\n  FilterBar,\n  type FilterBarProps,\n  type FilterBarVariant,\n} from \"@/components/ui/uai/filter-bar\";\nimport {\n  MetricCard,\n  type MetricCardProps,\n  type MetricCardVariant,\n} from \"@/components/ui/uai/metric-card\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const APPROVAL_QUEUE_VARIANTS = [\"grouped\", \"board\", \"compact\"] as const;\nexport type ApprovalQueueVariant = (typeof APPROVAL_QUEUE_VARIANTS)[number];\nexport type ApprovalQueueProps = ComponentProps<\"section\"> & { variant?: ApprovalQueueVariant };\n\ntype QueueContext = { id: string; variant: ApprovalQueueVariant };\nconst Context = createContext<QueueContext | null>(null);\nfunction useQueue(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within ApprovalQueue`);\n  return context;\n}\ntype GroupByContext = { value: string };\nconst GroupByCtx = createContext<GroupByContext | null>(null);\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\nconst GroupContext = createContext<string | null>(null);\nfunction useGroup(part: string) {\n  const id = useContext(GroupContext);\n  if (!id) throw new Error(`${part} must be used within ApprovalQueueGroup`);\n  return id;\n}\n\nconst metricVariants: Record<ApprovalQueueVariant, MetricCardVariant> = {\n  grouped: \"card\",\n  board: \"card\",\n  compact: \"compact\",\n};\nconst filterVariants: Record<ApprovalQueueVariant, FilterBarVariant> = {\n  grouped: \"toolbar\",\n  board: \"toolbar\",\n  compact: \"compact\",\n};\nconst cardVariants: Record<ApprovalQueueVariant, ApprovalCardVariant> = {\n  grouped: \"detailed\",\n  board: \"compact\",\n  compact: \"compact\",\n};\nconst emptyVariants: Record<ApprovalQueueVariant, EmptyStateVariant> = {\n  grouped: \"card\",\n  board: \"card\",\n  compact: \"compact\",\n};\n\n/** Review queue of pending decisions, grouped by a consumer-chosen key. */\nexport function ApprovalQueue({\n  variant = \"grouped\",\n  className,\n  children,\n  ...props\n}: ApprovalQueueProps) {\n  const id = useId();\n  return (\n    <Context.Provider value={{ id, variant }}>\n      <section\n        aria-labelledby={`${id}-title`}\n        data-slot=\"approval-queue\"\n        className={cn(\n          \"grid min-w-0 content-start text-[13px]/[18px] text-foreground\",\n          variant === \"compact\" ? \"gap-2.5\" : \"gap-4\",\n          className,\n        )}\n        {...props}\n        data-variant={variant}\n      >\n        {children}\n      </section>\n    </Context.Provider>\n  );\n}\n\nexport function ApprovalQueueHeader({ className, ...props }: ComponentProps<\"div\">) {\n  useQueue(\"ApprovalQueueHeader\");\n  return (\n    <div\n      data-slot=\"approval-queue-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 ApprovalQueueHeading({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"approval-queue-heading\"\n      className={cn(\"grid min-w-0 flex-[1_1_240px] gap-1\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function ApprovalQueueTitle({ className, ...props }: ComponentProps<\"h2\">) {\n  const context = useQueue(\"ApprovalQueueTitle\");\n  const compact = context.variant === \"compact\";\n  return (\n    <h2\n      data-slot=\"approval-queue-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 ApprovalQueueDescription({ className, ...props }: ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"approval-queue-description\"\n      className={cn(\"m-0 text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function ApprovalQueueActions({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"approval-queue-actions\"\n      className={cn(\"flex flex-wrap items-center gap-1.5\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function ApprovalQueueAction({\n  emphasis = \"secondary\",\n  type = \"button\",\n  className,\n  ...props\n}: ComponentProps<\"button\"> & { emphasis?: \"primary\" | \"secondary\" }) {\n  const context = useQueue(\"ApprovalQueueAction\");\n  const compact = context.variant === \"compact\";\n  return (\n    <Button\n      data-slot=\"approval-queue-action\"\n      variant={emphasis === \"primary\" ? \"default\" : \"secondary\"}\n      className={cn(\n        \"gap-1.5 rounded-full border-0 py-0 transition-[filter,box-shadow,scale] duration-[120ms,120ms,140ms] ease-[ease-out,ease-out,cubic-bezier(0.23,1,0.32,1)] focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid not-disabled:active:scale-[0.97] disabled:pointer-events-auto disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none motion-reduce:not-disabled:active:scale-100 [&_svg:not([class*='size-'])]:size-3.5\",\n        compact\n          ? \"h-6.5 px-2.75 text-[12px]/4 has-[>svg]:px-2.75\"\n          : \"h-7.5 px-3.25 text-[12.5px]/4 has-[>svg]:px-3.25\",\n        emphasis === \"primary\"\n          ? \"bg-primary text-primary-foreground hover:bg-primary not-disabled:hover:brightness-108\"\n          : \"bg-secondary text-foreground hover:bg-secondary not-disabled:hover:shadow-[inset_0_0_0_999px_color-mix(in_oklab,var(--foreground)_9%,transparent)]\",\n        className,\n      )}\n      {...props}\n      type={type}\n      data-emphasis={emphasis}\n    />\n  );\n}\n\n/** Queue totals. Metric cards reflow from four columns to one. */\nexport function ApprovalQueueSummary({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useQueue(\"ApprovalQueueSummary\");\n  const compact = context.variant === \"compact\";\n  return (\n    <div\n      data-slot=\"approval-queue-summary\"\n      className={cn(\n        \"grid min-w-0\",\n        compact\n          ? \"grid-cols-[repeat(auto-fit,minmax(min(100%,130px),1fr))] gap-2\"\n          : \"grid-cols-[repeat(auto-fit,minmax(min(100%,160px),1fr))] gap-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ApprovalQueueMetric(props: Omit<MetricCardProps, \"variant\">) {\n  const context = useQueue(\"ApprovalQueueMetric\");\n  return <MetricCard {...props} variant={metricVariants[context.variant]} />;\n}\n\n/** Filters. Compose Filter Bar parts inside it. */\nexport function ApprovalQueueFilters(props: Omit<FilterBarProps, \"variant\">) {\n  const context = useQueue(\"ApprovalQueueFilters\");\n  return <FilterBar {...props} variant={filterVariants[context.variant]} />;\n}\n\nexport type ApprovalQueueGroupByProps = Omit<ComponentProps<\"fieldset\">, \"defaultValue\"> & {\n  label?: string;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n};\n\n/** A radio group that chooses the grouping key, such as risk, age, owner, or impact. */\nexport function ApprovalQueueGroupBy({\n  label = \"Group by\",\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  className,\n  children,\n  ref: forwardedRef,\n  ...props\n}: ApprovalQueueGroupByProps) {\n  useQueue(\"ApprovalQueueGroupBy\");\n  const name = useId();\n  const [internal, setInternal] = useState(defaultValue);\n  const current = value ?? internal;\n  const ref = useRef<HTMLFieldSetElement | null>(null);\n  const [thumb, setThumb] = useState<{ x: number; y: number; width: number; height: number }>();\n  // Measure the checked option so one thumb can slide between options.\n  useIsomorphicLayoutEffect(() => {\n    const group = ref.current;\n    if (!group) return;\n    const measure = () => {\n      const checked = group.querySelector<HTMLElement>(\n        \"[data-slot=approval-queue-group-by-option][data-checked]\",\n      );\n      if (!checked || checked.offsetWidth === 0) return setThumb(undefined);\n      setThumb({\n        x: checked.offsetLeft,\n        y: checked.offsetTop,\n        width: checked.offsetWidth,\n        height: checked.offsetHeight,\n      });\n    };\n    measure();\n    if (typeof ResizeObserver !== \"function\") return;\n    const observer = new ResizeObserver(measure);\n    observer.observe(group);\n    return () => observer.disconnect();\n  }, [current]);\n  return (\n    <GroupByCtx.Provider value={{ value: current }}>\n      <RadioGroup\n        asChild\n        name={name}\n        value={current}\n        onValueChange={(next) => {\n          if (value === undefined) setInternal(next);\n          onValueChange?.(next);\n        }}\n        className={cn(\n          \"group/group-by relative isolate m-0 flex flex-wrap items-center gap-0.5 rounded-full border-0 bg-card p-0.75 shadow-[inset_0_0_0_1px_var(--border)]\",\n          className,\n        )}\n      >\n        <fieldset\n          data-slot=\"approval-queue-group-by\"\n          {...props}\n          ref={(node) => {\n            ref.current = node;\n            if (typeof forwardedRef === \"function\") forwardedRef(node);\n            else if (forwardedRef) forwardedRef.current = node;\n          }}\n          data-ready={thumb ? \"\" : undefined}\n        >\n          <legend className=\"float-left pr-1.5 pl-2.5 text-[12px] text-subtle-foreground\">\n            {label}\n          </legend>\n          {thumb ? (\n            <span\n              aria-hidden=\"true\"\n              data-slot=\"approval-queue-group-by-thumb\"\n              className=\"pointer-events-none absolute top-0 left-0 -z-1 rounded-full bg-accent shadow-[0_1px_2px_oklch(0_0_0/0.08)] transition-[translate,width] duration-240 ease-out-quint motion-reduce:transition-none\"\n              style={{\n                translate: `${thumb.x}px ${thumb.y}px`,\n                width: thumb.width,\n                height: thumb.height,\n              }}\n            />\n          ) : null}\n          {children}\n        </fieldset>\n      </RadioGroup>\n    </GroupByCtx.Provider>\n  );\n}\n\nexport type ApprovalQueueGroupByOptionProps = Omit<\n  ComponentProps<typeof RadioGroupItem>,\n  \"value\" | \"children\"\n> & { value: string; children?: ReactNode };\n\nexport function ApprovalQueueGroupByOption({\n  value,\n  children,\n  className,\n  ...props\n}: ApprovalQueueGroupByOptionProps) {\n  const group = useContext(GroupByCtx);\n  if (!group)\n    throw new Error(\"ApprovalQueueGroupByOption must be used within ApprovalQueueGroupBy\");\n  const checked = group.value === value;\n  return (\n    <Label\n      data-slot=\"approval-queue-group-by-option\"\n      data-checked={checked || undefined}\n      className={cn(\n        \"relative inline-flex h-6 cursor-pointer items-center rounded-full px-2.5 text-[12px] leading-[inherit] font-medium select-auto [transition:background-color_160ms_cubic-bezier(0.23,1,0.32,1),color_120ms_ease-out,box-shadow_160ms_cubic-bezier(0.23,1,0.32,1)] has-focus-visible:outline-2 has-focus-visible:outline-offset-1 has-focus-visible:outline-ring motion-reduce:transition-none\",\n        // The shared thumb paints the checked option once measured; until then it paints itself.\n        checked\n          ? \"bg-accent text-foreground shadow-[0_1px_2px_oklch(0_0_0/0.08)] group-data-ready/group-by:bg-transparent group-data-ready/group-by:shadow-none\"\n          : \"bg-transparent text-subtle-foreground hover:text-foreground\",\n        className,\n      )}\n    >\n      <RadioGroupItem\n        {...props}\n        value={value}\n        className=\"absolute inset-0 m-0 size-auto cursor-pointer rounded-full border-0 opacity-0 shadow-none focus-visible:ring-0 dark:bg-transparent\"\n      />\n      {children}\n    </Label>\n  );\n}\n\n/** Lays out groups as stacked sections, or as board columns in Board. */\nexport function ApprovalQueueGroups({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useQueue(\"ApprovalQueueGroups\");\n  return (\n    <div\n      data-slot=\"approval-queue-groups\"\n      className={cn(\n        \"grid min-w-0 items-start\",\n        context.variant === \"board\"\n          ? \"grid-cols-[repeat(auto-fit,minmax(min(100%,260px),1fr))]\"\n          : \"grid-cols-[minmax(0,1fr)]\",\n        context.variant === \"compact\" ? \"gap-3\" : \"gap-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ApprovalQueueGroup({ className, ...props }: ComponentProps<\"section\">) {\n  const context = useQueue(\"ApprovalQueueGroup\");\n  const id = useId();\n  const board = context.variant === \"board\";\n  return (\n    <GroupContext.Provider value={id}>\n      <section\n        aria-labelledby={id}\n        data-slot=\"approval-queue-group\"\n        className={cn(\n          \"grid min-w-0 content-start\",\n          context.variant === \"compact\" ? \"gap-1.5\" : \"gap-2.5\",\n          board\n            ? \"rounded-[14px] bg-[color-mix(in_oklab,var(--muted)_45%,var(--card))] p-2\"\n            : \"rounded-none bg-transparent p-0\",\n          className,\n        )}\n        {...props}\n      />\n    </GroupContext.Provider>\n  );\n}\n\nexport function ApprovalQueueGroupHeader({ className, ...props }: ComponentProps<\"div\">) {\n  useGroup(\"ApprovalQueueGroupHeader\");\n  return (\n    <div\n      data-slot=\"approval-queue-group-header\"\n      className={cn(\"flex flex-wrap items-center gap-2 px-1 py-0.5\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function ApprovalQueueGroupTitle({ className, ...props }: ComponentProps<\"h3\">) {\n  const id = useGroup(\"ApprovalQueueGroupTitle\");\n  return (\n    <h3\n      data-slot=\"approval-queue-group-title\"\n      className={cn(\"m-0 text-[13px]/[18px] font-medium\", className)}\n      {...props}\n      id={id}\n    />\n  );\n}\n\nexport function ApprovalQueueGroupCount({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"approval-queue-group-count\"\n      className={cn(\n        \"min-w-5 rounded-full bg-muted px-1.5 text-center text-[11.5px]/[18px] font-medium text-muted-foreground tabular-nums\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ApprovalQueueItems({ className, ...props }: ComponentProps<\"ul\">) {\n  const id = useGroup(\"ApprovalQueueItems\");\n  const context = useQueue(\"ApprovalQueueItems\");\n  return (\n    <ul\n      aria-labelledby={id}\n      data-slot=\"approval-queue-items\"\n      className={cn(\n        \"m-0 grid min-w-0 list-none p-0\",\n        context.variant === \"compact\" ? \"gap-1.5\" : \"gap-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\ntype DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;\nexport type ApprovalQueueItemProps = DistributiveOmit<ApprovalCardProps, \"variant\">;\n\n/** One pending decision. Compose Approval Card parts inside it. */\nexport function ApprovalQueueItem(props: ApprovalQueueItemProps) {\n  const context = useQueue(\"ApprovalQueueItem\");\n  return (\n    <li\n      data-slot=\"approval-queue-item\"\n      className=\"min-w-0 animate-in duration-240 ease-out-quint fade-in-0 slide-in-from-bottom-1 fill-mode-both nth-2:[animation-delay:40ms] nth-3:[animation-delay:80ms] nth-4:[animation-delay:120ms] nth-5:[animation-delay:160ms] nth-[n+6]:[animation-delay:200ms] motion-reduce:animate-none\"\n    >\n      <ApprovalCard {...(props as ApprovalCardProps)} variant={cardVariants[context.variant]} />\n    </li>\n  );\n}\n\n/** Shown when nothing is waiting. Compose Empty State parts inside it. */\nexport function ApprovalQueueEmpty(props: Omit<EmptyStateProps, \"variant\">) {\n  const context = useQueue(\"ApprovalQueueEmpty\");\n  return <EmptyState {...props} variant={emptyVariants[context.variant]} />;\n}\n",
      "type": "registry:component",
      "target": "@components/uai/approval-queue.tsx"
    }
  ],
  "categories": [
    "operations",
    "approvals",
    "review"
  ],
  "type": "registry:block"
}