{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-dock",
  "type": "registry:component",
  "title": "Command Dock",
  "description": "A small button that opens into a field, with the answer arriving under it a row at a time.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/panelui/command-dock.tsx",
      "type": "registry:component",
      "target": "@components/panelui/command-dock.tsx",
      "content": "'use client';\n\n/**\n * CommandDock — a small button that opens into a place to ask for something,\n * with the answer arriving under it a row at a time.\n *\n * ```tsx\n * <CommandDock onSubmit={ask}>\n *   <CommandDock.Trigger>Ask</CommandDock.Trigger>\n *   <CommandDock.Panel>\n *     <CommandDock.Input placeholder=\"What do you want to find?\" />\n *     <CommandDock.Results>\n *       {rows.map((row) => (\n *         <CommandDock.Row key={row.id}>\n *           <CommandDock.RowTitle>{row.title}</CommandDock.RowTitle>\n *           <CommandDock.RowMeta>{row.where}</CommandDock.RowMeta>\n *         </CommandDock.Row>\n *       ))}\n *     </CommandDock.Results>\n *   </CommandDock.Panel>\n * </CommandDock>\n * ```\n *\n * ## One surface that opens\n *\n * The button and the panel are the same surface. Closed, it is a pill as\n * wide as its label. Opened, it widens, the label fades into a field in the\n * same place, and it grows downwards as rows arrive. Closing runs the same\n * steps backwards: the rows fold up, then it narrows. Nothing appears from\n * elsewhere, so it is clear what was pressed and where the answer belongs.\n *\n * ## It runs nothing\n *\n * The dock does not know where answers come from. `onSubmit` is handed what\n * was typed, and the rows are whatever you render. A row that mounts slides\n * in after the one before it, so rows added one by one read as an answer\n * being written, whether they come from a search, a model or a list you\n * already had.\n *\n * ## Closing\n *\n * Escape closes it and gives focus back to where it was, and so does a press\n * anywhere outside it.\n */\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type FormEvent,\n  type ReactNode,\n} from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type * as ReactTypes from 'react';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nconst commandDockVariants = tv({\n  slots: {\n    root: 'relative flex w-full justify-center',\n    /*\n     * The widths are both real lengths, so the change between them can be\n     * animated; `auto` cannot. The easing has no overshoot: a surface this\n     * size that bounces as it closes reads as a mistake.\n     */\n    surface:\n      'overflow-hidden border bg-card text-card-foreground shadow-sm transition-[width,border-radius,box-shadow] duration-[400ms] ease-[cubic-bezier(0.2,0,0,1)] motion-reduce:transition-none',\n    // The label keeps its row whether or not it can be seen, so the surface\n    // is never shorter than one row and never jumps as the two trade places.\n    trigger:\n      'flex h-11 w-full cursor-pointer items-center justify-center gap-2 whitespace-nowrap px-4 text-sm font-medium outline-none transition-[opacity,background-color] duration-200 hover:bg-accent focus-visible:bg-accent [&_svg]:size-4 [&_svg]:shrink-0',\n    // Pulled up over the label's row: the field sits exactly where the label\n    // was, and the two fade through each other.\n    panel: '-mt-11 flex flex-col',\n    form: 'flex h-11 items-center gap-2 pe-1.5 ps-4 transition-opacity duration-200 motion-reduce:transition-none',\n    input: 'h-full min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground',\n    submit:\n      'inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full bg-primary text-primary-foreground outline-none transition-[opacity,scale] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card active:scale-95 disabled:cursor-default disabled:opacity-30 [&_svg]:size-4',\n    // Two rows of a grid, the second going from no height to its own, which\n    // is how the panel grows to whatever the rows come to.\n    results: 'grid transition-[grid-template-rows] duration-300 ease-out motion-reduce:transition-none',\n    resultsClip: 'min-h-0 overflow-hidden',\n    resultsInner: 'flex flex-col gap-0.5 border-t p-1.5',\n    row: 'flex items-center gap-3 rounded-xl px-2.5 py-2 text-sm transition-[opacity,translate] duration-300 ease-out starting:translate-y-1.5 starting:opacity-0 motion-reduce:transition-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground',\n    // A row's subject takes the room and truncates; its meta keeps its width.\n    rowTitle: 'min-w-0 flex-1 truncate font-medium',\n    rowMeta: 'shrink-0 text-muted-foreground',\n    status: 'flex items-center gap-2 px-2.5 py-2 text-sm text-muted-foreground',\n    dots: 'flex gap-1',\n    dot: 'size-1 animate-pulse rounded-full bg-muted-foreground',\n  },\n  variants: {\n    /*\n     * Opening and closing are the same steps in opposite orders. Opening, the\n     * surface widens and the rows follow it down. Closing, the rows fold up\n     * first and the surface narrows after them, so what is seen is one thing\n     * putting itself away and not two things changing at once.\n     */\n    open: {\n      true: {\n        // One length, not a width and a limit on it: a change of width is\n        // animated from the width as written, and `100%` with a `max-width`\n        // would start the closing from the full width of whatever it is in.\n        surface: 'w-[min(100%,28rem)] rounded-3xl shadow-lg',\n        trigger: 'pointer-events-none opacity-0',\n        form: 'opacity-100 delay-150',\n      },\n      false: {\n        surface: 'w-[var(--pu-closed)] rounded-[1.375rem] delay-200',\n        trigger: 'opacity-100 delay-300',\n        form: 'pointer-events-none opacity-0',\n      },\n    },\n    filled: {\n      true: { results: 'grid-rows-[1fr] delay-200' },\n      false: { results: 'grid-rows-[0fr]' },\n    },\n    interactive: {\n      true: { row: 'cursor-pointer hover:bg-accent focus-visible:bg-accent focus-visible:outline-none' },\n    },\n  },\n});\n\ninterface CommandDockContextValue {\n  open: boolean;\n  setOpen(open: boolean): void;\n  value: string;\n  setValue(value: string): void;\n  submit(): void;\n  busy: boolean;\n  panelId: string;\n}\n\nconst CommandDockContext = createContext<CommandDockContextValue | null>(null);\n\nfunction useDock(part: string): CommandDockContextValue {\n  const dock = useContext(CommandDockContext);\n  if (!dock) throw new Error(`${part} must be used inside a <CommandDock>.`);\n  return dock;\n}\n\nexport interface CommandDockProps extends Omit<ComponentProps<'div'>, 'onSubmit' | 'defaultValue'> {\n  /** Whether it is open, when the caller holds that. */\n  open?: boolean;\n  /** Whether it starts open, when the caller does not. */\n  defaultOpen?: boolean;\n  /** Called when it opens or closes. */\n  onOpenChange?: (open: boolean) => void;\n  /** What is typed in the field, when the caller holds it. */\n  value?: string;\n  /** Called with what is typed, each time it changes. */\n  onValueChange?: (value: string) => void;\n  /** Called with what was typed when it is sent. */\n  onSubmit?: (value: string) => void;\n  /** While true, the field cannot be sent again and `CommandDock.Status` is shown. */\n  busy?: boolean;\n  /** How wide it is while closed, as a CSS length. */\n  closedWidth?: string;\n}\n\nfunction CommandDockRoot({\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange,\n  value: controlledValue,\n  onValueChange,\n  onSubmit,\n  busy = false,\n  closedWidth = '9rem',\n  className,\n  style,\n  children,\n  ...props\n}: CommandDockProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const [internalValue, setInternalValue] = useState('');\n  const open = controlledOpen ?? internalOpen;\n  const value = controlledValue ?? internalValue;\n  const root = useRef<HTMLDivElement>(null);\n  const panelId = useId();\n  const slots = commandDockVariants({ open });\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (controlledOpen === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [controlledOpen, onOpenChange]\n  );\n\n  const setValue = useCallback(\n    (next: string) => {\n      if (controlledValue === undefined) setInternalValue(next);\n      onValueChange?.(next);\n    },\n    [controlledValue, onValueChange]\n  );\n\n  const submit = useCallback(() => {\n    const text = value.trim();\n    if (text && !busy) onSubmit?.(text);\n  }, [value, busy, onSubmit]);\n\n  // A press outside closes it. Escape does too, and puts focus back on the\n  // surface so the next key goes somewhere sensible.\n  useEffect(() => {\n    if (!open) return;\n    const onPointerDown = (event: PointerEvent) => {\n      if (!root.current?.contains(event.target as Node)) setOpen(false);\n    };\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key !== 'Escape' || !root.current?.contains(document.activeElement)) return;\n      setOpen(false);\n      requestAnimationFrame(() => root.current?.querySelector<HTMLElement>('[data-dock-trigger]')?.focus());\n    };\n    document.addEventListener('pointerdown', onPointerDown);\n    document.addEventListener('keydown', onKeyDown);\n    return () => {\n      document.removeEventListener('pointerdown', onPointerDown);\n      document.removeEventListener('keydown', onKeyDown);\n    };\n  }, [open, setOpen]);\n\n  const context = useMemo(\n    () => ({ open, setOpen, value, setValue, submit, busy, panelId }),\n    [open, setOpen, value, setValue, submit, busy, panelId]\n  );\n\n  return (\n    <CommandDockContext.Provider value={context}>\n      <div ref={root} className={slots.root({ className })} {...props}>\n        <div\n          data-open={open ? '' : undefined}\n          className={slots.surface()}\n          style={{ ['--pu-closed' as string]: closedWidth, ...style }}\n        >\n          {children}\n        </div>\n      </div>\n    </CommandDockContext.Provider>\n  );\n}\nCommandDockRoot.displayName = 'CommandDock';\n\n/** Keeps an element out of the tab order and the accessibility tree while it cannot be seen. */\nfunction useInert<T extends HTMLElement>(inert: boolean) {\n  const ref = useRef<T>(null);\n  // Before anything else runs: the field asks for focus as the dock opens,\n  // and an element inside something still inert cannot take it.\n  useLayoutEffect(() => {\n    // Set on the element, not passed as a prop: how a boolean `inert` prop is\n    // written to the page differs between React versions.\n    ref.current?.toggleAttribute('inert', inert);\n  }, [inert]);\n  return ref;\n}\n\nexport interface CommandDockTriggerProps extends ComponentProps<'button'> {}\n\n/** The closed form: a button that opens the dock. */\nfunction CommandDockTrigger({ className, onClick, render, ...props }: CommandDockTriggerProps & RenderProp<'button'>) {\n  const dock = useDock('CommandDock.Trigger');\n  const ref = useInert<HTMLButtonElement>(dock.open);\n  return useRender({\n    defaultTagName: 'button',\n    render,\n    props: mergeProps(\n      {\n        ref: ref,\n        type: 'button',\n        'data-dock-trigger': '',\n        'aria-expanded': dock.open,\n        'aria-controls': dock.panelId,\n        className: commandDockVariants({ open: dock.open }).trigger({ className }),\n        onClick: (event: ReactTypes.MouseEvent<HTMLButtonElement>) => {\n          onClick?.(event);\n          if (!event.defaultPrevented) dock.setOpen(true);\n        },\n      },\n      props\n    ),\n  });\n}\nCommandDockTrigger.displayName = 'CommandDock.Trigger';\n\nexport interface CommandDockPanelProps extends ComponentProps<'div'> {}\n\n/** The open form: the field, and the rows under it. */\nfunction CommandDockPanel({ className, render, ...props }: CommandDockPanelProps & RenderProp<'div'>) {\n  const dock = useDock('CommandDock.Panel');\n  // Kept in the page while closed, so it can be seen folding away. Inert, so\n  // nothing in it can be reached while it cannot be seen.\n  const ref = useInert<HTMLDivElement>(!dock.open);\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        ref: ref,\n        id: dock.panelId,\n        'data-open': dock.open ? '' : undefined,\n        className: commandDockVariants().panel({ className }),\n      },\n      props\n    ),\n  });\n}\nCommandDockPanel.displayName = 'CommandDock.Panel';\n\nexport interface CommandDockInputProps extends Omit<ComponentProps<'input'>, 'value' | 'onChange' | 'type'> {\n  /** What a screen reader calls the send button. */\n  submitLabel?: string;\n}\n\n/** The field, with a button to send what is in it. Enter sends too. */\nfunction CommandDockInput({\n  className,\n  submitLabel = 'Send',\n  'aria-label': ariaLabel = 'Ask',\n  ...props\n}: CommandDockInputProps) {\n  const dock = useDock('CommandDock.Input');\n  const slots = commandDockVariants({ open: dock.open });\n  const field = useRef<HTMLInputElement>(null);\n\n  // It is opened in order to type in it. Asked for without scrolling: the\n  // surface is still on its way to its size, and the page should not chase it.\n  useEffect(() => {\n    if (dock.open) field.current?.focus({ preventScroll: true });\n  }, [dock.open]);\n\n  const send = (event: FormEvent) => {\n    event.preventDefault();\n    dock.submit();\n  };\n\n  return (\n    <form role=\"search\" className={slots.form()} onSubmit={send}>\n      <input\n        ref={field}\n        type=\"text\"\n        aria-label={ariaLabel}\n        value={dock.value}\n        onChange={(event) => dock.setValue(event.target.value)}\n        className={slots.input({ className })}\n        {...props}\n      />\n      <button\n        type=\"submit\"\n        aria-label={submitLabel}\n        disabled={dock.busy || dock.value.trim() === ''}\n        className={slots.submit()}\n      >\n        <svg\n          aria-hidden=\"true\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2.25\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <path d=\"M12 19V5M5 12l7-7 7 7\" />\n        </svg>\n      </button>\n    </form>\n  );\n}\nCommandDockInput.displayName = 'CommandDock.Input';\n\nexport interface CommandDockResultsProps extends ComponentProps<'div'> {\n  /** What a screen reader calls the list of rows. */\n  label?: string;\n}\n\n/** Where the rows go. It takes no room until there is something in it. */\nfunction CommandDockResults({\n  className,\n  label = 'Results',\n  children,\n  render,\n  ...props\n}: CommandDockResultsProps & RenderProp<'div'>) {\n  const dock = useDock('CommandDock.Results');\n  const rows = Array.isArray(children) ? children.flat().filter(Boolean).length : children ? 1 : 0;\n  // Closed, it folds away whatever it holds.\n  const filled = dock.open && (rows > 0 || dock.busy);\n  const slots = commandDockVariants({ filled });\n  const element = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        role: 'list',\n        'aria-label': label,\n        'aria-live': 'polite',\n        className: slots.resultsInner({ className }),\n        children: <>{children}</>,\n      },\n      props\n    ),\n  });\n  return (\n    <div className={slots.results()}>\n      <div className={slots.resultsClip()}>\n        {/* Polite: rows are read as they arrive, without cutting in on typing. */}\n        {element}\n      </div>\n    </div>\n  );\n}\nCommandDockResults.displayName = 'CommandDock.Results';\n\nexport interface CommandDockRowProps extends ComponentProps<'div'> {\n  /** Called when the row is pressed. Passing this makes the row a button. */\n  onSelect?: () => void;\n}\n\n/** One row of the answer. It slides in when it is added. */\nfunction CommandDockRow({ className, onSelect, children, render, ...props }: CommandDockRowProps & RenderProp<'div'>) {\n  const slots = commandDockVariants({ interactive: onSelect !== undefined });\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        role: 'listitem',\n        children: (\n          <>\n            {onSelect ? (\n              <button\n                type=\"button\"\n                onClick={onSelect}\n                className={slots.row({ className: ['w-full text-start', className] })}\n              >\n                {children}\n              </button>\n            ) : (\n              <div className={slots.row({ className })}>{children}</div>\n            )}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nCommandDockRow.displayName = 'CommandDock.Row';\n\nexport interface CommandDockRowTitleProps extends ComponentProps<'span'> {}\n\n/** What a row is, in a line that takes the row's spare width and truncates. */\nfunction CommandDockRowTitle({ className, render, ...props }: CommandDockRowTitleProps & RenderProp<'span'>) {\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'command-dock-row-title', className: commandDockVariants().rowTitle({ className }) },\n      props\n    ),\n  });\n}\nCommandDockRowTitle.displayName = 'CommandDock.RowTitle';\n\nexport interface CommandDockRowMetaProps extends ComponentProps<'span'> {}\n\n/** Something short at the end of a row, quieter than its title: a date, a place. */\nfunction CommandDockRowMeta({ className, render, ...props }: CommandDockRowMetaProps & RenderProp<'span'>) {\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'command-dock-row-meta', className: commandDockVariants().rowMeta({ className }) },\n      props\n    ),\n  });\n}\nCommandDockRowMeta.displayName = 'CommandDock.RowMeta';\n\nexport interface CommandDockStatusProps extends ComponentProps<'div'> {\n  children?: ReactNode;\n}\n\n/** Shown under the rows while `busy`: three dots, and words if you give it some. */\nfunction CommandDockStatus({\n  className,\n  children = 'Working',\n  render,\n  ...props\n}: CommandDockStatusProps & RenderProp<'div'>) {\n  const slots = commandDockVariants();\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        role: 'status',\n        className: slots.status({ className }),\n        children: (\n          <>\n            <span aria-hidden=\"true\" className={slots.dots()}>\n              <span className={slots.dot()} />\n              <span className={slots.dot({ className: '[animation-delay:150ms]' })} />\n              <span className={slots.dot({ className: '[animation-delay:300ms]' })} />\n            </span>\n            {children}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nCommandDockStatus.displayName = 'CommandDock.Status';\n\nexport const CommandDock = Object.assign(CommandDockRoot, {\n  Trigger: CommandDockTrigger,\n  Panel: CommandDockPanel,\n  Input: CommandDockInput,\n  Results: CommandDockResults,\n  Row: CommandDockRow,\n  RowTitle: CommandDockRowTitle,\n  RowMeta: CommandDockRowMeta,\n  Status: CommandDockStatus,\n});\n\n/* By name as well, for a Server Component, which cannot dot into a client\n   module. */\nexport {\n  CommandDockTrigger,\n  CommandDockPanel,\n  CommandDockInput,\n  CommandDockResults,\n  CommandDockRow,\n  CommandDockRowTitle,\n  CommandDockRowMeta,\n  CommandDockStatus,\n};\n"
    }
  ]
}
