{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"reasoning","type":"registry:ui","title":"Reasoning","description":"What an AI model thought before it answered, folded under one line: \"Thinking…\" while it works, then how long it took.","dependencies":["lucide-react","radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json","https://dashral.com/r/lib-motion.json"],"files":[{"path":"registry/dashral/reasoning.tsx","type":"registry:file","target":"components/dashral/reasoning.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\"\nimport { Brain, ChevronDown } from \"lucide-react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { reducedMotion } from \"./lib/motion\"\nimport { cn } from \"./lib/utils\"\n\n/** \"thinking\" while the model works through it (the text may still be streaming in); \"done\" once it's answering. */\ntype ReasoningStatus = \"thinking\" | \"done\"\n\ntype ReasoningProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"title\"> & {\n  /** The model's reasoning, as it streams in: text (line breaks kept) or your own rendering. */\n  children?: React.ReactNode\n  status?: ReasoningStatus\n  /** How long it thought, in seconds. Measured from \"thinking\" to \"done\" when left out. */\n  duration?: number\n  /** Controlled open state. Uncontrolled, it opens while thinking and closes when done (see autoClose). */\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  /** Close when the thinking is done, unless the reader opened or closed it themselves. On by default. */\n  autoClose?: boolean\n  /** Replaces \"Thinking…\" and \"Thought for 8 seconds\". */\n  label?: React.ReactNode\n  classNames?: { trigger?: string; content?: string }\n}\n\n/** Text with a light sweeping across it while something is in progress; still, and plainly muted, with reduced motion. */\nfunction Shimmer({ children }: { children: React.ReactNode }) {\n  const ref = React.useRef<HTMLSpanElement>(null)\n  React.useEffect(() => {\n    const el = ref.current\n    if (!el || reducedMotion() || typeof el.animate !== \"function\") return\n    // The Web Animations API, so it needs no keyframes in the stylesheet (the registry ships components, not CSS).\n    // The band crosses the text in the first 70% of each turn, then rests a moment; with the text, in the reading\n    // direction (right to left in Arabic).\n    const rtl = getComputedStyle(el).direction === \"rtl\"\n    const [from, to] = rtl ? [\"0% 0\", \"100% 0\"] : [\"100% 0\", \"0% 0\"]\n    const sweep = el.animate([{ backgroundPosition: from }, { backgroundPosition: to, offset: 0.7 }, { backgroundPosition: to }], {\n      duration: 1800,\n      iterations: Infinity,\n      easing: \"ease-in-out\",\n    })\n    return () => sweep.cancel()\n  }, [])\n  return (\n    <span\n      ref={ref}\n      // The muted text with a band of light across it: toward the page's background in light mode (a bright gleam on\n      // grey), toward the text color in dark mode. Darkening it instead reads as a shadow and barely shows.\n      className={cn(\n        \"bg-[length:250%_100%] bg-clip-text text-transparent motion-reduce:bg-none motion-reduce:text-muted-foreground\",\n        \"bg-[linear-gradient(90deg,var(--muted-foreground)_30%,color-mix(in_oklab,var(--muted-foreground),var(--background)_80%)_50%,var(--muted-foreground)_70%)]\",\n        \"dark:bg-[linear-gradient(90deg,var(--muted-foreground)_30%,var(--foreground)_50%,var(--muted-foreground)_70%)]\"\n      )}\n    >\n      {children}\n    </span>\n  )\n}\n\n/**\n * What the model thought before it answered, folded under one line: \"Thinking…\" with a shimmer while it works (open,\n * so the thoughts can be followed), then \"Thought for 8 seconds\", closed. Click the line to read them again.\n */\nfunction Reasoning({\n  children,\n  status = \"done\",\n  duration,\n  open: openProp,\n  defaultOpen,\n  onOpenChange,\n  autoClose = true,\n  label,\n  className,\n  classNames,\n  ...props\n}: ReasoningProps) {\n  const { t } = useLocale()\n  const thinking = status === \"thinking\"\n  const [ownOpen, setOwnOpen] = React.useState(defaultOpen ?? thinking)\n  const open = openProp ?? ownOpen\n  // Once the reader has opened or closed it, it stays the way they left it.\n  const touched = React.useRef(false)\n  const setOpen = (next: boolean) => {\n    if (openProp === undefined) setOwnOpen(next)\n    onOpenChange?.(next)\n  }\n\n  // How long the thinking took, when not given: from the render that said \"thinking\" to the one that said \"done\".\n  const startedAt = React.useRef<number | null>(null)\n  const [measured, setMeasured] = React.useState<number | null>(null)\n  const previous = React.useRef(status)\n  React.useEffect(() => {\n    if (thinking && startedAt.current === null) startedAt.current = Date.now()\n    if (previous.current === \"thinking\" && status === \"done\") {\n      if (startedAt.current !== null) setMeasured(Math.round((Date.now() - startedAt.current) / 1000))\n      if (autoClose && !touched.current) setOpen(false)\n    }\n    if (previous.current === \"done\" && thinking && !touched.current) setOpen(true)\n    previous.current = status\n    // setOpen only changes with the props it reads; running on status alone is what's wanted.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [status])\n\n  const seconds = duration ?? measured\n  const title = label ?? (thinking ? t(\"thinking\") : seconds ? t(\"thoughtFor\", { count: seconds }) : t(\"thoughtBriefly\"))\n  const hasContent = React.Children.toArray(children).length > 0\n\n  return (\n    <CollapsiblePrimitive.Root\n      data-slot=\"reasoning\"\n      data-status={status}\n      open={open}\n      onOpenChange={(next) => {\n        touched.current = true\n        setOpen(next)\n      }}\n      className={cn(\"text-sm\", className)}\n      {...props}\n    >\n      <CollapsiblePrimitive.Trigger\n        disabled={!hasContent}\n        className={cn(\n          \"group/reasoning inline-flex items-center gap-1.5 rounded-md py-1 text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-default disabled:hover:text-muted-foreground\",\n          classNames?.trigger\n        )}\n      >\n        <Brain aria-hidden className=\"size-4 shrink-0\" />\n        {/* The line changes from \"Thinking…\" to how long it took; screen readers hear the change once. */}\n        <span aria-live=\"polite\">{thinking && !label ? <Shimmer>{title}</Shimmer> : title}</span>\n        {hasContent && (\n          <ChevronDown\n            aria-hidden\n            className=\"size-3.5 shrink-0 transition-transform duration-(--duration-standard) ease-standard group-data-[state=open]/reasoning:rotate-180 motion-reduce:transition-none\"\n          />\n        )}\n      </CollapsiblePrimitive.Trigger>\n      <CollapsiblePrimitive.Content\n        className=\"overflow-hidden duration-(--duration-standard) ease-standard data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down motion-reduce:animate-none\"\n      >\n        <div\n          data-slot=\"reasoning-content\"\n          dir=\"auto\"\n          className={cn(\n            \"mt-1 ms-2 border-s-2 ps-3.5 leading-relaxed text-muted-foreground [overflow-wrap:anywhere]\",\n            typeof children === \"string\" && \"whitespace-pre-wrap\",\n            classNames?.content\n          )}\n        >\n          {children}\n        </div>\n      </CollapsiblePrimitive.Content>\n    </CollapsiblePrimitive.Root>\n  )\n}\n\nexport { Reasoning, type ReasoningProps, type ReasoningStatus }\n"}],"cssVars":{"theme":{"ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{}}}