{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tips-panel","type":"registry:ui","title":"Tips Panel","description":"Grid of short tips with one action each – taken tips turn into a done state","dependencies":[],"files":[{"path":"registry/tips-panel/tips-panel.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useState, type ReactNode } from \"react\";\n\n/*\n * Tips Panel – a panel of short tips, each with one action, in a gray tray with a white card, in a single self-contained file.\n * Taking an action marks the tip as done; the list scrolls inside the white card, framed by the gray tray.\n * The styles are injected below, so no extra files are needed.\n */\n\nexport type Tip = {\n  id: string;\n  title: string;\n  /** Short explanation – may contain a <Kbd> */\n  description: ReactNode;\n  /** Button text */\n  action: string;\n  /** Text after the action was taken, e.g. \"Enabled\" */\n  doneLabel?: string;\n  /** Shows the action as the dark button – use once for the most important tip */\n  primary?: boolean;\n  onAction?: () => void;\n};\n\nexport type TipsPanelProps = {\n  title?: string;\n  tips: Tip[];\n  /** Shows the Close button in the footer */\n  onClose?: () => void;\n  closeLabel?: string;\n  className?: string;\n};\n\nexport function TipsPanel({ title = \"Tips\", tips, onClose, closeLabel = \"Close\", className }: TipsPanelProps) {\n  const [done, setDone] = useState<Set<string>>(new Set());\n  // Tips that share an id still get their own key, so marking one never marks the other\n  const seen = new Map<string, number>();\n  const keyed = tips.map((tip) => {\n    const n = seen.get(tip.id) ?? 0;\n    seen.set(tip.id, n + 1);\n    return { tip, key: n ? `${tip.id}#${n}` : tip.id };\n  });\n  // Only tips that are still in the list count\n  const doneCount = keyed.filter((k) => done.has(k.key)).length;\n\n  return (\n    <>\n      <style href=\"ui-tips-panel\" precedence=\"default\">\n        {css}\n      </style>\n      <section className={[\"ui-tp\", className].filter(Boolean).join(\" \")} aria-label={title}>\n        <header className=\"ui-tp__head\">\n          <h3 className=\"ui-tp__title\" dir=\"auto\">\n            {title}\n          </h3>\n          <span className=\"ui-tp__count\" aria-live=\"polite\">\n            {count.format(doneCount)}/{count.format(tips.length)} done\n          </span>\n        </header>\n\n        <div className=\"ui-tp__card\">\n          <ul className=\"ui-tp__grid\">\n            {keyed.map(({ tip, key }, i) => {\n              const isDone = done.has(key);\n              return (\n                <li key={key} className=\"ui-tp__tip\" data-done={isDone || undefined} style={{ animationDelay: `${80 + Math.min(i, 12) * 50}ms` }}>\n                  <p className=\"ui-tp__tip-title\" dir=\"auto\">\n                    {tip.title}\n                  </p>\n                  <p className=\"ui-tp__tip-text\" dir=\"auto\">\n                    {tip.description}\n                  </p>\n                  {isDone ? (\n                    <span className=\"ui-tp__done\" role=\"status\">\n                      <CheckIcon />\n                      {tip.doneLabel ?? \"Done\"}\n                    </span>\n                  ) : (\n                    <button\n                      type=\"button\"\n                      className={[\"ui-tp__btn\", tip.primary && \"ui-tp__btn--primary\"].filter(Boolean).join(\" \")}\n                      onClick={() => {\n                        setDone((d) => new Set(d).add(key));\n                        tip.onAction?.();\n                      }}\n                    >\n                      {tip.action}\n                    </button>\n                  )}\n                </li>\n              );\n            })}\n          </ul>\n        </div>\n\n        {onClose && (\n          <footer className=\"ui-tp__foot\">\n            <button type=\"button\" className=\"ui-tp__btn\" onClick={onClose}>\n              {closeLabel}\n            </button>\n          </footer>\n        )}\n      </section>\n    </>\n  );\n}\n\nconst count = new Intl.NumberFormat(\"en\");\n\n/** Keyboard key for tip descriptions, e.g. <Kbd>⌘K</Kbd> */\nexport const Kbd = ({ children }: { children: ReactNode }) => <kbd className=\"ui-tp__kbd\">{children}</kbd>;\n\nconst CheckIcon = () => (\n  <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" fill=\"currentColor\" />\n    <path d=\"m5 8.2 2 2 4-4.2\" fill=\"none\" stroke=\"#fff\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n  </svg>\n);\n\n/* ---------- Styles ---------- */\n\nconst css = /* css */ `\n.ui-tp {\n  --ui-tp-tray: var(--aui-subtle, #f0f0f0);\n  --ui-tp-card: var(--aui-surface, #ffffff);\n  --ui-tp-line: var(--aui-border, #ececec);\n  --ui-tp-ink: var(--aui-ink, #171717);\n  --ui-tp-muted: var(--aui-muted, #737373);\n  --ui-tp-ok: var(--aui-blue, #4fb0fe);\n  --ui-tp-focus: var(--aui-focus, rgba(0, 96, 226, 0.7));\n  /* Height of the tip list before it scrolls */\n  --ui-tp-max-height: 420px;\n  --ui-tp-ease: var(--aui-ease, cubic-bezier(0.22, 1, 0.36, 1));\n  box-sizing: border-box;\n  width: 100%;\n  max-width: 760px;\n  /* Like the Chat Composer: a gray tray with heading and footer, the white card sits on top of it */\n  background: var(--ui-tp-tray);\n  /* The gray tray frames the white card on every side */\n  padding: 6px;\n  border-radius: calc(var(--aui-radius-lg, 16px) + 6px);\n  font-family: var(--aui-font, \"Inter\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  color: var(--ui-tp-ink);\n  -webkit-font-smoothing: antialiased;\n}\n.ui-tp *, .ui-tp *::before, .ui-tp *::after { box-sizing: border-box; }\n.ui-tp button { margin: 0; font: inherit; cursor: pointer; }\n.ui-tp button:focus-visible { outline: 2px solid var(--ui-tp-focus); outline-offset: 2px; }\n\n.ui-tp__head {\n  display: flex;\n  align-items: baseline;\n  justify-content: space-between;\n  gap: 12px;\n  padding: 13px 20px 12px;\n}\n.ui-tp__title {\n  min-width: 0;\n  margin: 0;\n  overflow-wrap: anywhere;\n  font-size: 16px;\n  font-weight: 600;\n  line-height: 22px;\n  letter-spacing: -0.01em;\n}\n.ui-tp__count {\n  flex: none;\n  font-size: 13px;\n  font-variant-numeric: tabular-nums;\n  color: var(--ui-tp-muted);\n}\n\n.ui-tp__card {\n  container-type: inline-size;\n  max-height: var(--ui-tp-max-height);\n  overflow-y: auto;\n  scrollbar-width: thin;\n  background: var(--ui-tp-card);\n  border: 1px solid var(--aui-border, #e5e5e5);\n  border-radius: var(--aui-radius-lg, 16px);\n  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);\n}\n.ui-tp__grid {\n  display: grid;\n  grid-template-columns: repeat(3, minmax(0, 1fr));\n  gap: 12px;\n  margin: 0;\n  padding: 14px;\n  list-style: none;\n}\n@container (max-width: 560px) { .ui-tp__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }\n@container (max-width: 380px) { .ui-tp__grid { grid-template-columns: minmax(0, 1fr); } }\n\n.ui-tp__tip {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  padding: 16px 16px 14px;\n  border: 1px solid var(--ui-tp-line);\n  border-radius: calc(var(--aui-radius-lg, 16px) - 2px);\n  animation: ui-tp-in 480ms var(--ui-tp-ease) both;\n  transition: background-color 260ms ease, border-color 260ms ease;\n}\n.ui-tp__tip[data-done] { background: var(--aui-subtle, #fafafa); }\n/* Tips are shrink-to-fit flex items – max-width keeps long words and URLs inside the tile */\n.ui-tp__tip-title {\n  max-width: 100%;\n  margin: 0;\n  overflow-wrap: anywhere;\n  text-align: start;\n  font-size: 14px;\n  font-weight: 500;\n  line-height: 20px;\n}\n.ui-tp__tip-text {\n  flex: 1;\n  max-width: 100%;\n  overflow-wrap: anywhere;\n  white-space: pre-line;\n  text-align: start;\n  margin: 6px 0 14px;\n  font-size: 13px;\n  line-height: 20px;\n  color: var(--ui-tp-muted);\n}\n.ui-tp__kbd {\n  display: inline-grid;\n  place-items: center;\n  min-width: 20px;\n  height: 20px;\n  padding: 0 5px;\n  border-radius: calc(var(--aui-radius-sm, 7px) - 1px);\n  background: var(--aui-surface, #fff);\n  box-shadow: 0 0 0 1px var(--aui-border, #e5e5e5), 0 1px 0 var(--aui-border, #e5e5e5);\n  font-family: inherit;\n  font-size: 11px;\n  font-weight: 500;\n  line-height: 1;\n  color: var(--ui-tp-ink);\n  vertical-align: 1px;\n}\n\n/* A long label wraps inside the tile instead of leaving it; one line stays 30px */\n.ui-tp__btn {\n  max-width: 100%;\n  min-height: 30px;\n  padding: 5px 12px;\n  border: 0;\n  border-radius: calc(var(--aui-radius, 10px) - 1px);\n  background: var(--aui-surface, #fff);\n  box-shadow: 0 0 0 1px var(--aui-border, #e5e5e5), 0 1px 2px rgba(0, 0, 0, 0.04);\n  font-size: 13px !important;\n  font-weight: 500;\n  line-height: 20px;\n  color: var(--ui-tp-ink);\n  text-align: start;\n  overflow-wrap: anywhere;\n  transition: background-color 160ms ease, transform 160ms ease;\n}\n.ui-tp__btn:hover { background: #f7f7f7; }\n.ui-tp__btn:active { transform: scale(0.98); }\n.ui-tp__btn--primary {\n  background: linear-gradient(#323137, #201e25);\n  box-shadow: 0 0 0 1px #0d0d0d, 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.12);\n  color: #fff;\n}\n.ui-tp__btn--primary:hover { background: linear-gradient(#3b3a41, #26242b); }\n\n.ui-tp__done {\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  max-width: 100%;\n  min-height: 30px;\n  overflow-wrap: anywhere;\n  font-size: 13px;\n  font-weight: 500;\n  color: var(--ui-tp-ok);\n  animation: ui-tp-pop 360ms var(--ui-tp-ease);\n}\n\n.ui-tp__foot { padding: 8px 10px 10px; }\n.ui-tp:not(:has(.ui-tp__foot)) { padding-bottom: 8px; }\n\n@keyframes ui-tp-in {\n  from { opacity: 0; transform: translateY(4px); }\n}\n@keyframes ui-tp-pop {\n  from { opacity: 0; transform: scale(0.94); }\n}\n@media (prefers-reduced-motion: reduce) {\n  .ui-tp__tip, .ui-tp__done { animation: none; }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> */\n:where(.dark, [data-theme=\"dark\"]) .ui-tp {\n  --ui-tp-tray: var(--aui-subtle, #161618);\n  --ui-tp-card: var(--aui-surface, #232326);\n  --ui-tp-line: var(--aui-border, rgba(255, 255, 255, 0.08));\n  --ui-tp-ink: var(--aui-ink, #ededed);\n  --ui-tp-muted: var(--aui-muted, #8f8f8f);\n  --ui-tp-ok: var(--aui-blue, #4fb0fe);\n  --ui-tp-focus: var(--aui-focus, rgba(77, 141, 255, 0.8));\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-tp__card {\n  border-color: var(--aui-border, rgba(255, 255, 255, 0.08));\n  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);\n  color-scheme: dark;\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-tp__tip[data-done] { background: var(--aui-subtle, #2a2a2d); }\n:where(.dark, [data-theme=\"dark\"]) .ui-tp__kbd {\n  background: var(--aui-surface, #2a2a2d);\n  box-shadow: 0 0 0 1px var(--aui-border, rgba(255, 255, 255, 0.1)), 0 1px 0 var(--aui-border, rgba(255, 255, 255, 0.1));\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-tp__btn {\n  background: var(--aui-surface, #2a2a2d);\n  box-shadow: 0 0 0 1px var(--aui-border, rgba(255, 255, 255, 0.1)), 0 1px 2px rgba(0, 0, 0, 0.4);\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-tp__btn:hover { background: #313135; }\n/* The dark primary button inverts, like on the site */\n:where(.dark, [data-theme=\"dark\"]) .ui-tp__btn--primary {\n  background: linear-gradient(#f4f4f5, #e4e4e7);\n  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 1px 0 #ffffff;\n  color: #121213;\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-tp__btn--primary:hover { background: linear-gradient(#ffffff, #ececef); }\n`;\n"}]}