{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tips-panel-clean","type":"registry:ui","title":"Tips Panel (Clean)","description":"Grid of short tips with one action each – taken tips turn into a done state","dependencies":[],"files":[{"path":"registry/tips-panel-clean/tips-panel-clean.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useState, type ReactNode } from \"react\";\n\n/*\n * Tips Panel (clean style) – a panel of short tips, each with one action, in one white card, in a single self-contained file.\n * Taking an action marks the tip as done; only the tip list scrolls – the heading and the Close row stay in place.\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-clean\" precedence=\"default\">\n        {css}\n      </style>\n      <section className={[\"ui-tpc\", className].filter(Boolean).join(\" \")} aria-label={title}>\n        <header className=\"ui-tpc__head\">\n          <h3 className=\"ui-tpc__title\" dir=\"auto\">\n            {title}\n          </h3>\n          <span className=\"ui-tpc__count\" aria-live=\"polite\">\n            {count.format(doneCount)}/{count.format(tips.length)} done\n          </span>\n        </header>\n\n        <div className=\"ui-tpc__body\">\n          <ul className=\"ui-tpc__grid\">\n            {keyed.map(({ tip, key }, i) => {\n              const isDone = done.has(key);\n              return (\n                <li key={key} className=\"ui-tpc__tip\" data-done={isDone || undefined} style={{ animationDelay: `${80 + Math.min(i, 12) * 50}ms` }}>\n                  <p className=\"ui-tpc__tip-title\" dir=\"auto\">\n                    {tip.title}\n                  </p>\n                  <p className=\"ui-tpc__tip-text\" dir=\"auto\">\n                    {tip.description}\n                  </p>\n                  {isDone ? (\n                    <span className=\"ui-tpc__done\" role=\"status\">\n                      <CheckIcon />\n                      {tip.doneLabel ?? \"Done\"}\n                    </span>\n                  ) : (\n                    <button\n                      type=\"button\"\n                      className={[\"ui-tpc__btn\", tip.primary && \"ui-tpc__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-tpc__foot\">\n            <button type=\"button\" className=\"ui-tpc__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-tpc__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=\"6.5\" fill=\"currentColor\" />\n    <path d=\"m5.6 8.1 1.7 1.7 3.2-3.4\" fill=\"none\" stroke=\"#fff\" strokeWidth=\"1.3\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n  </svg>\n);\n\n/* ---------- Styles ---------- */\n\nconst css = /* css */ `\n/* Colors and motion as variables – override them on .ui-tpc */\n.ui-tpc {\n  --ui-tpc-card: var(--aui-surface, #ffffff);\n  --ui-tpc-strong: var(--aui-ink, #000000);\n  --ui-tpc-ink: var(--aui-ink, rgba(0, 0, 0, 0.88));\n  --ui-tpc-text: var(--aui-muted, rgba(0, 0, 0, 0.61));\n  --ui-tpc-muted: var(--aui-muted, rgba(0, 0, 0, 0.45));\n  --ui-tpc-line: var(--aui-border, rgba(0, 0, 0, 0.06));\n  --ui-tpc-wash: var(--aui-subtle, rgba(0, 0, 0, 0.015));\n  --ui-tpc-hover: rgba(0, 0, 0, 0.03);\n  --ui-tpc-ok: var(--aui-success, #1e8a4f);\n  --ui-tpc-primary: var(--aui-primary, rgba(0, 0, 0, 0.88));\n  --ui-tpc-primary-hover: rgba(0, 0, 0, 0.8);\n  --ui-tpc-on-primary: var(--aui-on-primary, #fcfcfc);\n  /* Strokes are shadows: a 0.5px hairline plus a soft lift */\n  --ui-tpc-shadow: var(--aui-shadow-card, 0 0 0 0.5px rgba(0, 0, 0, 0.06), 0 1px 1px -1px rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.05));\n  --ui-tpc-shadow-hover: 0 0 0 0.5px rgba(0, 0, 0, 0.1), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.07);\n  --ui-tpc-focus: var(--aui-focus, rgba(0, 96, 226, 0.7));\n  /* Height of the tip list before it scrolls */\n  --ui-tpc-max-height: 420px;\n  --ui-tpc-ease: var(--aui-ease, cubic-bezier(0.22, 1, 0.36, 1));\n  box-sizing: border-box;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  max-width: 760px;\n  /* One white card – the edge is a shadow, not a border */\n  border-radius: var(--aui-radius-lg, 10px);\n  background: var(--ui-tpc-card);\n  box-shadow: var(--ui-tpc-shadow);\n  font-family: var(--aui-font, \"Inter\", \"InterVariable\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  font-size: 13px;\n  line-height: 20px;\n  font-weight: 400;\n  color: var(--ui-tpc-ink);\n  text-align: left;\n  -webkit-font-smoothing: antialiased;\n}\n.ui-tpc *, .ui-tpc *::before, .ui-tpc *::after { box-sizing: border-box; }\n:where(.ui-tpc) button { margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; }\n.ui-tpc button:focus-visible { outline: 2px solid var(--ui-tpc-focus); outline-offset: 2px; }\n:where(.ui-tpc) svg { display: block; flex: none; }\n\n/* ----- Head ----- */\n.ui-tpc__head {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 12px;\n  padding: 12px 16px;\n  border-bottom: 0.5px solid var(--ui-tpc-line);\n}\n.ui-tpc__title {\n  min-width: 0;\n  margin: 0;\n  overflow-wrap: anywhere;\n  font-size: 15px;\n  font-weight: 500;\n  line-height: 24px;\n  color: var(--ui-tpc-strong);\n}\n.ui-tpc__count {\n  flex: none;\n  font-size: 12px;\n  line-height: 16px;\n  font-variant-numeric: tabular-nums;\n  color: var(--ui-tpc-muted);\n  white-space: nowrap;\n}\n\n/* ----- The tip list: the only part that scrolls ----- */\n.ui-tpc__body {\n  container-type: inline-size;\n  max-height: var(--ui-tpc-max-height);\n  overflow-y: auto;\n  scrollbar-width: thin;\n}\n.ui-tpc__grid {\n  display: grid;\n  grid-template-columns: repeat(3, minmax(0, 1fr));\n  gap: 8px;\n  margin: 0;\n  padding: 12px;\n  list-style: none;\n}\n@container (max-width: 560px) { .ui-tpc__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }\n@container (max-width: 380px) { .ui-tpc__grid { grid-template-columns: minmax(0, 1fr); } }\n\n/* Each tip is a quiet tile: a hairline drawn as an inset shadow */\n.ui-tpc__tip {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  padding: 12px;\n  border-radius: calc(var(--aui-radius-lg, 10px) - 2px);\n  box-shadow: inset 0 0 0 0.5px var(--ui-tpc-line);\n  animation: ui-tpc-in 480ms var(--ui-tpc-ease) both;\n  transition: background-color 260ms ease;\n}\n.ui-tpc__tip[data-done] { background: var(--ui-tpc-wash); }\n/* Tips are shrink-to-fit flex items – max-width keeps long words and URLs inside the tile */\n.ui-tpc__tip-title {\n  max-width: 100%;\n  margin: 0;\n  overflow-wrap: anywhere;\n  text-align: start;\n  font-size: 13px;\n  font-weight: 500;\n  line-height: 20px;\n  color: var(--ui-tpc-strong);\n}\n.ui-tpc__tip-text {\n  flex: 1;\n  max-width: 100%;\n  overflow-wrap: anywhere;\n  white-space: pre-line;\n  text-align: start;\n  margin: 2px 0 12px;\n  font-size: 13px;\n  line-height: 20px;\n  color: var(--ui-tpc-text);\n}\n.ui-tpc__kbd {\n  display: inline-grid;\n  place-items: center;\n  min-width: 20px;\n  height: 18px;\n  padding: 0 5px;\n  border-radius: var(--aui-radius-sm, 5px);\n  background: var(--ui-tpc-card);\n  box-shadow: var(--ui-tpc-shadow);\n  font-family: inherit;\n  font-size: 11px;\n  font-weight: 500;\n  line-height: 1;\n  color: var(--ui-tpc-ink);\n  vertical-align: 1px;\n}\n\n/* Secondary: white with the card shadow */\n/* A long label wraps inside the tile instead of leaving it; one line stays 28px */\n.ui-tpc__btn {\n  display: inline-flex;\n  align-items: center;\n  max-width: 100%;\n  min-height: 28px;\n  padding: 4px 10px;\n  border-radius: calc(var(--aui-radius, 7px) - 1px);\n  background: var(--ui-tpc-card);\n  box-shadow: var(--ui-tpc-shadow);\n  font-size: 13px !important;\n  font-weight: 500;\n  line-height: 20px;\n  color: var(--ui-tpc-strong);\n  text-align: start;\n  overflow-wrap: anywhere;\n  transition: box-shadow 140ms ease, background-color 140ms ease, transform 160ms var(--ui-tpc-ease);\n}\n.ui-tpc__btn:hover { box-shadow: var(--ui-tpc-shadow-hover); }\n.ui-tpc__btn:active { transform: scale(0.98); }\n/* Primary: the dark chip */\n.ui-tpc__btn--primary {\n  background: var(--ui-tpc-primary);\n  box-shadow: none;\n  color: var(--ui-tpc-on-primary);\n}\n.ui-tpc__btn--primary:hover { background: var(--ui-tpc-primary-hover); box-shadow: none; }\n\n.ui-tpc__done {\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  max-width: 100%;\n  min-height: 28px;\n  overflow-wrap: anywhere;\n  font-size: 13px;\n  font-weight: 500;\n  line-height: 20px;\n  color: var(--ui-tpc-ok);\n  animation: ui-tpc-pop 360ms var(--ui-tpc-ease);\n}\n\n/* ----- The last row inside the card ----- */\n.ui-tpc__foot {\n  display: flex;\n  justify-content: flex-end;\n  padding: 8px;\n  border-top: 0.5px solid var(--ui-tpc-line);\n}\n\n@keyframes ui-tpc-in {\n  from { opacity: 0; transform: translateY(4px); }\n}\n@keyframes ui-tpc-pop {\n  from { opacity: 0; transform: scale(0.94); }\n}\n\n@media (pointer: coarse) {\n  .ui-tpc__btn, .ui-tpc__done { min-height: 34px; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .ui-tpc__tip, .ui-tpc__done { animation: none; }\n  .ui-tpc__tip, .ui-tpc__btn { transition: 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-tpc {\n  --ui-tpc-card: var(--aui-surface, #1c1c1e);\n  --ui-tpc-strong: var(--aui-ink, #ffffff);\n  --ui-tpc-ink: var(--aui-ink, rgba(255, 255, 255, 0.9));\n  --ui-tpc-text: var(--aui-muted, rgba(255, 255, 255, 0.62));\n  --ui-tpc-muted: var(--aui-muted, rgba(255, 255, 255, 0.46));\n  --ui-tpc-line: var(--aui-border, rgba(255, 255, 255, 0.08));\n  --ui-tpc-wash: var(--aui-subtle, rgba(255, 255, 255, 0.03));\n  --ui-tpc-hover: rgba(255, 255, 255, 0.04);\n  --ui-tpc-ok: var(--aui-green, #5fc88a);\n  --ui-tpc-primary: var(--aui-primary, rgba(255, 255, 255, 0.9));\n  --ui-tpc-primary-hover: rgba(255, 255, 255, 0.8);\n  --ui-tpc-on-primary: var(--aui-on-primary, #1c1c1e);\n  --ui-tpc-shadow: var(--aui-shadow-card, 0 0 0 0.5px rgba(255, 255, 255, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.4));\n  --ui-tpc-shadow-hover: 0 0 0 0.5px rgba(255, 255, 255, 0.16), 0 1px 2px 0 rgba(0, 0, 0, 0.4);\n  --ui-tpc-focus: var(--aui-focus, rgba(77, 141, 255, 0.8));\n  color-scheme: dark;\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-tpc__btn:not(.ui-tpc__btn--primary) { background: var(--aui-surface, #232326); }\n`;\n"}]}