{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "statements",
    "finance",
    "accounting"
  ],
  "dependencies": [
    "@base-ui-components/react"
  ],
  "description": "Fiscal-aware. Never assumes January.",
  "files": [
    {
      "content": "\"use client\";\n\nimport { Popover } from \"@base-ui-components/react/popover\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport {\n  fiscalQuarterOf,\n  fiscalQuartersOf,\n  fiscalYearOf,\n  formatFiscalLabel,\n  formatMonthLabel,\n  formatRange,\n  type PeriodPreset,\n  type PeriodRange,\n  recentMonths,\n  resolvePreset,\n} from \"@/lib/ledger/fiscal-period\";\nimport { CalendarDaysIcon, LockClosedIcon } from \"@/components/ledger/icons\";\n\nexport type PeriodSelection = { label: string; range: PeriodRange };\n\nexport interface PeriodPickerProps {\n  className?: string;\n  /** Closed periods are marked and warn on selection. */\n  closedThrough?: Date;\n  /** FY26 Q1 may start in July. Never assume January. */\n  fiscalYearStartMonth?: number;\n  /**\n   * How many complete months to list individually. A close is worked one month\n   * at a time and the month wanted is very often not one of the two a preset\n   * names — \"August, no, July\" is the most common thing anyone does in this\n   * control, and making them type two dates for it is the reason people give\n   * up on period pickers.\n   *\n   * 24 rather than 12 because the comparative a review asks for is the same\n   * month a year earlier, and that month has to be reachable.\n   */\n  months?: number;\n  onValueChange: (p: PeriodSelection) => void;\n  /** Offer the four quarters of the fiscal year as their own list. */\n  showFiscalQuarters?: boolean;\n  today?: Date;\n  value: PeriodSelection;\n}\n\n/**\n * Grouped, because a flat list of seventeen is a list nobody reads to the end\n * of. The groups are the four questions anyone actually arrives with: which\n * month, which quarter, which year, and over what window.\n *\n * `ytd` and `fytd` both appear, and deliberately: for an entity whose year\n * starts in July they are different numbers, and offering one of them under\n * the name \"year to date\" hands the reader the wrong one half the time. When\n * the fiscal year *is* the calendar year the two collapse, and the second is\n * dropped rather than listed twice.\n */\nconst PRESET_GROUPS: { heading: string; presets: { key: PeriodPreset; label: string }[] }[] = [\n  {\n    heading: \"Month\",\n    presets: [\n      { key: \"mtd\", label: \"Month to date\" },\n      { key: \"this-month\", label: \"This month\" },\n      { key: \"last-month\", label: \"Last month\" },\n      { key: \"same-month-last-year\", label: \"Same month last year\" },\n    ],\n  },\n  {\n    heading: \"Quarter\",\n    presets: [\n      { key: \"qtd\", label: \"Quarter to date\" },\n      { key: \"this-fiscal-quarter\", label: \"This fiscal quarter\" },\n      { key: \"last-fiscal-quarter\", label: \"Last fiscal quarter\" },\n      { key: \"same-quarter-last-year\", label: \"Same quarter last year\" },\n    ],\n  },\n  {\n    heading: \"Year\",\n    presets: [\n      { key: \"ytd\", label: \"Year to date\" },\n      { key: \"fytd\", label: \"Fiscal year to date\" },\n      { key: \"this-fiscal-year\", label: \"This fiscal year\" },\n      { key: \"last-fiscal-year\", label: \"Last fiscal year\" },\n      { key: \"last-calendar-year\", label: \"Last calendar year\" },\n    ],\n  },\n  {\n    heading: \"Trailing\",\n    presets: [\n      { key: \"trailing-3\", label: \"Trailing 3 months\" },\n      { key: \"trailing-6\", label: \"Trailing 6 months\" },\n      { key: \"trailing-12\", label: \"Trailing 12 months\" },\n      { key: \"trailing-24\", label: \"Trailing 24 months\" },\n    ],\n  },\n];\n\n/** Every preset shows its resolved dates. */\nexport function PeriodPicker({\n  value,\n  onValueChange,\n  fiscalYearStartMonth = 1,\n  today = new Date(),\n  closedThrough,\n  months = 24,\n  showFiscalQuarters = true,\n  className,\n}: PeriodPickerProps) {\n  const [open, setOpen] = React.useState(false);\n  const { quarter, fiscalYear } = fiscalQuarterOf(today, fiscalYearStartMonth);\n  const calendarFiscalYear = fiscalYearStartMonth === 1;\n\n  /* Newest first. The month wanted is nearly always a recent one, and a list\n     that opens on a month two years ago is a list you scroll before you read. */\n  const monthOptions = React.useMemo(\n    () =>\n      recentMonths(today, months)\n        .reverse()\n        .map((range) => ({ label: formatMonthLabel(range.start), range })),\n    [today, months],\n  );\n\n  const quarterOptions = React.useMemo(() => {\n    if (!showFiscalQuarters) return [];\n    const fy = fiscalYearOf(today, fiscalYearStartMonth);\n    return fiscalQuartersOf(fy, fiscalYearStartMonth).map((q) => ({\n      label: formatFiscalLabel(q.quarter, fy, fiscalYearStartMonth),\n      range: q.range,\n    }));\n  }, [today, fiscalYearStartMonth, showFiscalQuarters]);\n\n  const choose = (label: string, range: PeriodRange) => {\n    onValueChange({ label, range });\n    setOpen(false);\n  };\n\n  /* One row, one shape, whether it came from a preset, a month or a quarter —\n     three renderers would be three chances for one of them to stop showing\n     the resolved dates, which is the whole contract of this control. */\n  const option = (label: string, range: PeriodRange) => {\n    const closed = closedThrough != null && range.end <= closedThrough;\n    return (\n      <li key={label}>\n        <button\n          className={cn(\n            \"flex w-full items-center gap-2 rounded-xs px-2 py-2 text-left\",\n            \"transition-colors duration-1 ease-standard hover:bg-surface-hover\",\n            value.label === label && \"bg-surface-selected\",\n          )}\n          onClick={() => choose(label, range)}\n          type=\"button\"\n        >\n          <span className=\"flex-1 text-body\">{label}</span>\n          <span className=\"numeric text-caption text-text-secondary\">{formatRange(range)}</span>\n          {closed ? (\n            <LockClosedIcon aria-label=\"Closed period\" className=\"size-3 text-text-tertiary\" />\n          ) : null}\n        </button>\n      </li>\n    );\n  };\n\n  return (\n    <Popover.Root onOpenChange={setOpen} open={open}>\n      <Popover.Trigger\n        aria-label={`Period: ${value.label}, ${formatRange(value.range)}`}\n        className={cn(\n          \"inline-flex h-8 items-center gap-2 rounded-sm border border-border bg-surface px-3\",\n          \"text-label font-medium transition-colors duration-1 ease-standard hover:bg-surface-hover\",\n          className,\n        )}\n      >\n        <CalendarDaysIcon aria-hidden=\"true\" className=\"size-3.5 text-text-tertiary\" />\n        <span className=\"numeric\">{formatRange(value.range)}</span>\n      </Popover.Trigger>\n      <Popover.Portal>\n        <Popover.Positioner align=\"start\" className=\"z-dropdown\" sideOffset={8}>\n          <Popover.Popup\n            className=\"w-[340px] rounded-md border border-border bg-surface-raised p-2 shadow-elevation-3\"\n            style={{ transformOrigin: \"var(--transform-origin)\" }}\n          >\n            {fiscalYearStartMonth !== 1 ? (\n              <p className=\"px-2 pb-2 text-caption text-text-secondary\">\n                Fiscal year starts in month {fiscalYearStartMonth}. Currently{\" \"}\n                {formatFiscalLabel(quarter, fiscalYear, fiscalYearStartMonth)}.\n              </p>\n            ) : null}\n            {/* Capped and scrolled rather than allowed to grow: twenty-four\n              months and seventeen presets is taller than most viewports, and a\n              popup that runs off the bottom of the screen hides the custom\n              range at the end of it. */}\n            <div className=\"max-h-[360px] overflow-y-auto scrollbar-thin\">\n              {PRESET_GROUPS.map((group) => {\n                const presets = group.presets.filter(\n                  (preset) => !(calendarFiscalYear && preset.key === \"fytd\"),\n                );\n                return (\n                  <section key={group.heading}>\n                    <h3 className=\"px-2 pb-1 pt-2 text-caption font-medium text-text-tertiary\">\n                      {group.heading}\n                    </h3>\n                    <ul>\n                      {presets.map((preset) =>\n                        option(preset.label, resolvePreset(preset.key, today, fiscalYearStartMonth)),\n                      )}\n                    </ul>\n                  </section>\n                );\n              })}\n\n              {quarterOptions.length > 0 ? (\n                <section>\n                  <h3 className=\"px-2 pb-1 pt-2 text-caption font-medium text-text-tertiary\">\n                    {calendarFiscalYear\n                      ? `Quarters of ${fiscalYear}`\n                      : `Quarters of FY${String(fiscalYear).slice(-2)}`}\n                  </h3>\n                  <ul>{quarterOptions.map((q) => option(q.label, q.range))}</ul>\n                </section>\n              ) : null}\n\n              <section>\n                <h3 className=\"px-2 pb-1 pt-2 text-caption font-medium text-text-tertiary\">Months</h3>\n                <ul>{monthOptions.map((m) => option(m.label, m.range))}</ul>\n              </section>\n            </div>\n\n            <div className=\"mt-2 border-t border-border-subtle p-2\">\n              <p className=\"mb-2 text-caption text-text-secondary\">Custom range</p>\n              <div className=\"flex items-center gap-2\">\n                <input\n                  aria-label=\"Range start\"\n                  className=\"numeric h-8 flex-1 rounded-sm border border-border bg-surface px-2 text-label outline-none focus-visible:border-accent focus-visible:ring-2 focus-visible:ring-accent-subtle\"\n                  defaultValue={value.range.start.toISOString().slice(0, 10)}\n                  onChange={(e) =>\n                    onValueChange({\n                      label: \"Custom\",\n                      range: { ...value.range, start: new Date(`${e.target.value}T00:00:00`) },\n                    })\n                  }\n                  type=\"date\"\n                />\n                <span aria-hidden=\"true\" className=\"text-text-tertiary\">\n                  –\n                </span>\n                <input\n                  aria-label=\"Range end\"\n                  className=\"numeric h-8 flex-1 rounded-sm border border-border bg-surface px-2 text-label outline-none focus-visible:border-accent focus-visible:ring-2 focus-visible:ring-accent-subtle\"\n                  defaultValue={value.range.end.toISOString().slice(0, 10)}\n                  onChange={(e) =>\n                    onValueChange({\n                      label: \"Custom\",\n                      range: { ...value.range, end: new Date(`${e.target.value}T00:00:00`) },\n                    })\n                  }\n                  type=\"date\"\n                />\n              </div>\n            </div>\n\n            {closedThrough ? (\n              <p className=\"px-2 pt-2 text-caption text-warning\">\n                Periods through {formatRange({ end: closedThrough, start: closedThrough })} are closed. A\n                closed period’s numbers shouldn’t be changing.\n              </p>\n            ) : null}\n          </Popover.Popup>\n        </Popover.Positioner>\n      </Popover.Portal>\n    </Popover.Root>\n  );\n}\n",
      "path": "components/ledger/period-picker.tsx",
      "target": "components/ledger/period-picker.tsx",
      "type": "registry:component"
    }
  ],
  "name": "period-picker",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/fiscal-period",
    "@lopifi/icons"
  ],
  "title": "PeriodPicker",
  "type": "registry:component"
}
